You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by kx...@apache.org on 2015/09/18 00:10:27 UTC

chttpd commit: updated refs/heads/master to 933ba2e

Repository: couchdb-chttpd
Updated Branches:
  refs/heads/master e1c0f12d9 -> 933ba2e3a


Implement /db/_bulk_get endpoint

COUCHDB-2310


Project: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/commit/933ba2e3
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/tree/933ba2e3
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/diff/933ba2e3

Branch: refs/heads/master
Commit: 933ba2e3a2a77637b4ad275cf505b7d8a3ef0777
Parents: e1c0f12
Author: Alexander Shorin <kx...@apache.org>
Authored: Wed Apr 22 21:30:47 2015 +0300
Committer: Alexander Shorin <kx...@apache.org>
Committed: Fri Sep 18 01:08:16 2015 +0300

----------------------------------------------------------------------
 src/chttpd_db.erl                | 196 +++++++++++++++++++-
 test/chttpd_db_bulk_get_test.erl | 334 ++++++++++++++++++++++++++++++++++
 2 files changed, 526 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/933ba2e3/src/chttpd_db.erl
----------------------------------------------------------------------
diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl
index 7798deb..327e6d0 100644
--- a/src/chttpd_db.erl
+++ b/src/chttpd_db.erl
@@ -438,6 +438,36 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>], user_ctx=Ctx}=Req,
 db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->
     send_method_not_allowed(Req, "POST");
 
+
+db_req(#httpd{method='POST', path_parts=[_, <<"_bulk_get">>]}=Req, Db) ->
+    couch_stats:increment_counter([couchdb, httpd, bulk_requests]),
+    couch_httpd:validate_ctype(Req, "application/json"),
+    {JsonProps} = chttpd:json_body_obj(Req),
+    case couch_util:get_value(<<"docs">>, JsonProps) of
+        undefined ->
+            throw({bad_request, <<"Missing JSON list of 'docs'.">>});
+        Docs ->
+            #doc_query_args{
+                options = Options
+            } = bulk_get_parse_doc_query(Req),
+
+            {ok, Resp} = start_json_response(Req, 200),
+            send_chunk(Resp, <<"{\"results\": [">>),
+
+            lists:foldl(fun(Doc, Sep) ->
+                {DocId, Results, Options1} = bulk_get_open_doc_revs(Db, Doc,
+                                                                    Options),
+                bulk_get_send_docs_json(Resp, DocId, Results, Options1, Sep),
+                <<",">>
+            end, <<"">>, Docs),
+
+            send_chunk(Resp, <<"]}">>),
+            end_json_response(Resp)
+    end;
+db_req(#httpd{path_parts=[_, <<"_bulk_get">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "POST");
+
+
 db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) ->
     chttpd:validate_ctype(Req, "application/json"),
     {IdsRevs} = chttpd:json_body_obj(Req),
@@ -1291,8 +1321,10 @@ get_md5_header(Req) ->
     end.
 
 parse_doc_query(Req) ->
-    lists:foldl(fun({Key,Value}, Args) ->
-        case {Key, Value} of
+    lists:foldl(fun parse_doc_query/2, #doc_query_args{}, chttpd:qs(Req)).
+
+parse_doc_query({Key, Value}, Args) ->
+    case {Key, Value} of
         {"attachments", "true"} ->
             Options = [attachments | Args#doc_query_args.options],
             Args#doc_query_args{options=Options};
@@ -1345,8 +1377,7 @@ parse_doc_query(Req) ->
             Args#doc_query_args{options=Options};
         _Else -> % unknown key value pair, ignore.
             Args
-        end
-    end, #doc_query_args{}, chttpd:qs(Req)).
+    end.
 
 parse_changes_query(Req) ->
     erlang:erase(changes_seq_interval),
@@ -1525,6 +1556,163 @@ set_namespace(<<"_design_docs">>, Args) ->
 set_namespace(NS, #mrargs{extra = Extra} = Args) ->
     Args#mrargs{extra = [{namespace, NS} | Extra]}.
 
+
+%% /db/_bulk_get stuff
+
+bulk_get_parse_doc_query(Req) ->
+    lists:foldl(fun({Key, Value}, Args) ->
+        ok = validate_query_param(Key),
+        parse_doc_query({Key, Value}, Args)
+    end, #doc_query_args{}, chttpd:qs(Req)).
+
+
+validate_query_param("open_revs"=Key) ->
+    throw_bad_query_param(Key);
+validate_query_param("new_edits"=Key) ->
+    throw_bad_query_param(Key);
+validate_query_param("w"=Key) ->
+    throw_bad_query_param(Key);
+validate_query_param("rev"=Key) ->
+    throw_bad_query_param(Key);
+validate_query_param("atts_since"=Key) ->
+    throw_bad_query_param(Key);
+validate_query_param(_) ->
+    ok.
+
+throw_bad_query_param(Key) when is_list(Key) ->
+    throw_bad_query_param(?l2b(Key));
+throw_bad_query_param(Key) when is_binary(Key) ->
+    Msg = <<"\"", Key/binary, "\" query parameter is not acceptable">>,
+    throw({bad_request, Msg}).
+
+
+bulk_get_open_doc_revs(Db, {Props}, Options) ->
+    bulk_get_open_doc_revs1(Db, Props, Options, {}).
+
+
+bulk_get_open_doc_revs1(Db, Props, Options, {}) ->
+    case parse_field(<<"id">>, couch_util:get_value(<<"id">>, Props)) of
+        {error, {DocId, Error, Reason}} ->
+            {DocId, {error, {null, Error, Reason}}, Options};
+
+        {ok, undefined} ->
+            Error = {null, bad_request, <<"document id missed">>},
+            {null, {error, Error}, Options};
+
+        {ok, DocId} ->
+            bulk_get_open_doc_revs1(Db, Props, Options, {DocId})
+    end;
+bulk_get_open_doc_revs1(Db, Props, Options, {DocId}) ->
+    RevStr = couch_util:get_value(<<"rev">>, Props),
+
+    case parse_field(<<"rev">>, RevStr) of
+        {error, {RevStr, Error, Reason}} ->
+            {DocId, {error, {RevStr, Error, Reason}}, Options};
+
+        {ok, undefined} ->
+            bulk_get_open_doc_revs1(Db, Props, Options, {DocId, all});
+
+        {ok, Rev} ->
+            bulk_get_open_doc_revs1(Db, Props, Options, {DocId, [Rev]})
+    end;
+bulk_get_open_doc_revs1(Db, Props, Options, {DocId, Revs}) ->
+    AttsSinceStr = couch_util:get_value(<<"atts_since">>, Props),
+
+    case parse_field(<<"atts_since">>, AttsSinceStr) of
+        {error, {BadAttsSinceRev, Error, Reason}} ->
+            {DocId, {error, {BadAttsSinceRev, Error, Reason}}, Options};
+
+        {ok, []} ->
+            bulk_get_open_doc_revs1(Db, Props, Options, {DocId, Revs, Options});
+
+        {ok, RevList} ->
+            Options1 = [{atts_since, RevList}, attachments | Options],
+            bulk_get_open_doc_revs1(Db, Props, Options, {DocId, Revs, Options1})
+    end;
+bulk_get_open_doc_revs1(Db, Props, _, {DocId, Revs, Options}) ->
+    case fabric:open_revs(Db, DocId, Revs, Options) of
+        {ok, []} ->
+            RevStr = couch_util:get_value(<<"rev">>, Props),
+            Error = {RevStr, <<"not_found">>, <<"missing">>},
+            {DocId, {error, Error}, Options};
+        Results ->
+            {DocId, Results, Options}
+    end.
+
+
+parse_field(<<"id">>, undefined) ->
+    {ok, undefined};
+parse_field(<<"id">>, Value) ->
+    try
+        ok = couch_doc:validate_docid(Value),
+        {ok, Value}
+    catch
+        throw:{Error, Reason} ->
+            {error, {Value, Error, Reason}}
+    end;
+parse_field(<<"rev">>, undefined) ->
+    {ok, undefined};
+parse_field(<<"rev">>, Value) ->
+    try
+        Rev = couch_doc:parse_rev(Value),
+        {ok, Rev}
+    catch
+        throw:{bad_request=Error, Reason} ->
+            {error, {Value, Error, Reason}}
+    end;
+parse_field(<<"atts_since">>, undefined) ->
+    {ok, []};
+parse_field(<<"atts_since">>, []) ->
+    {ok, []};
+parse_field(<<"atts_since">>, Value) when is_list(Value) ->
+    parse_atts_since(Value, []);
+parse_field(<<"atts_since">>, Value) ->
+    {error, {Value, bad_request, <<"att_since value must be array of revs.">>}}.
+
+
+parse_atts_since([], Acc) ->
+    {ok, lists:reverse(Acc)};
+parse_atts_since([RevStr | Rest], Acc) ->
+    case parse_field(<<"rev">>, RevStr) of
+        {ok, Rev} ->
+            parse_atts_since(Rest, [Rev | Acc]);
+        {error, _}=Error ->
+            Error
+    end.
+
+
+bulk_get_send_docs_json(Resp, DocId, Results, Options, Sep) ->
+    Id = ?JSON_ENCODE(DocId),
+    send_chunk(Resp, [Sep, <<"{\"id\": ">>, Id, <<", \"docs\": [">>]),
+    bulk_get_send_docs_json1(Resp, DocId, Results, Options),
+    send_chunk(Resp, <<"]}">>).
+
+bulk_get_send_docs_json1(Resp, DocId, {error, {Rev, Error, Reason}}, _) ->
+    send_chunk(Resp, [bulk_get_json_error(DocId, Rev, Error, Reason)]);
+bulk_get_send_docs_json1(_Resp, _DocId, {ok, []}, _) ->
+    ok;
+bulk_get_send_docs_json1(Resp, DocId, {ok, Docs}, Options) ->
+    lists:foldl(fun(Result, AccSeparator) ->
+        case Result of
+            {ok, Doc} ->
+                JsonDoc = couch_doc:to_json_obj(Doc, Options),
+                Json = ?JSON_ENCODE({[{ok, JsonDoc}]}),
+                send_chunk(Resp, [AccSeparator, Json]);
+            {{Error, Reason}, RevId} ->
+                RevStr = couch_doc:rev_to_str(RevId),
+                Json = bulk_get_json_error(DocId, RevStr, Error, Reason),
+                send_chunk(Resp, [AccSeparator, Json])
+        end,
+        <<",">>
+    end, <<"">>, Docs).
+
+bulk_get_json_error(DocId, Rev, Error, Reason) ->
+    ?JSON_ENCODE({[{error, {[{<<"id">>, DocId},
+                             {<<"rev">>, Rev},
+                             {<<"error">>, Error},
+                             {<<"reason">>, Reason}]}}]}).
+
+
 -ifdef(TEST).
 -include_lib("eunit/include/eunit.hrl").
 

http://git-wip-us.apache.org/repos/asf/couchdb-chttpd/blob/933ba2e3/test/chttpd_db_bulk_get_test.erl
----------------------------------------------------------------------
diff --git a/test/chttpd_db_bulk_get_test.erl b/test/chttpd_db_bulk_get_test.erl
new file mode 100644
index 0000000..20d774d
--- /dev/null
+++ b/test/chttpd_db_bulk_get_test.erl
@@ -0,0 +1,334 @@
+%% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+%% use this file except in compliance with the License. You may obtain a copy of
+%% the License at
+%%
+%%   http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+%% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+%% License for the specific language governing permissions and limitations under
+%% the License.
+
+-module(chttpd_db_bulk_get_test).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+-define(TIMEOUT, 3000).
+
+
+setup() ->
+    mock(chttpd),
+    mock(couch_epi),
+    mock(couch_httpd),
+    mock(couch_stats),
+    mock(fabric),
+    mock(mochireq),
+    Pid = spawn_accumulator(),
+    Pid.
+
+
+teardown(Pid) ->
+    ok = stop_accumulator(Pid),
+    meck:unload(chttpd),
+    meck:unload(couch_epi),
+    meck:unload(couch_httpd),
+    meck:unload(couch_stats),
+    meck:unload(fabric),
+    meck:unload(mochireq).
+
+
+bulk_get_test_() ->
+    {
+        "/db/_bulk_get tests",
+        {
+            foreach, fun setup/0, fun teardown/1,
+            [
+                fun should_require_docs_field/1,
+                fun should_not_accept_specific_query_params/1,
+                fun should_return_empty_results_on_no_docs/1,
+                fun should_get_doc_with_all_revs/1,
+                fun should_validate_doc_with_bad_id/1,
+                fun should_validate_doc_with_bad_rev/1,
+                fun should_validate_missing_doc/1,
+                fun should_validate_bad_atts_since/1,
+                fun should_include_attachments_when_atts_since_specified/1
+            ]
+        }
+    }.
+
+
+should_require_docs_field(_) ->
+    Req = fake_request({[{}]}),
+    ?_assertThrow({bad_request, _}, chttpd_db:db_req(Req, nil)).
+
+
+should_not_accept_specific_query_params(_) ->
+    Req = fake_request({[{<<"docs">>, []}]}),
+    lists:map(fun (Param) ->
+        {Param, ?_assertThrow({bad_request, _},
+                              begin
+                                  ok = meck:expect(chttpd, qs,
+                                                   fun(_) -> [{Param, ""}] end),
+                                  chttpd_db:db_req(Req, nil)
+                              end)}
+    end, ["rev", "open_revs", "atts_since", "w", "new_edits"]).
+
+
+should_return_empty_results_on_no_docs(Pid) ->
+    Req = fake_request({[{<<"docs">>, []}]}),
+    chttpd_db:db_req(Req, nil),
+    Results = get_results_from_response(Pid),
+    ?_assertEqual([], Results).
+
+
+should_get_doc_with_all_revs(Pid) ->
+    DocId = <<"docudoc">>,
+    Req = fake_request(DocId),
+
+    RevA = {[{<<"_id">>, DocId}, {<<"_rev">>, <<"1-ABC">>}]},
+    RevB = {[{<<"_id">>, DocId}, {<<"_rev">>, <<"1-CDE">>}]},
+    DocRevA = #doc{id = DocId, body = {[{<<"_rev">>, <<"1-ABC">>}]}},
+    DocRevB = #doc{id = DocId, body = {[{<<"_rev">>, <<"1-CDE">>}]}},
+
+    mock_open_revs(all, {ok, [{ok, DocRevA}, {ok, DocRevB}]}),
+    chttpd_db:db_req(Req, nil),
+
+    [{Result}] = get_results_from_response(Pid),
+    ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)),
+
+    Docs = couch_util:get_value(<<"docs">>, Result),
+    ?assertEqual(2, length(Docs)),
+
+    [{DocA0}, {DocB0}] = Docs,
+
+    DocA = couch_util:get_value(<<"ok">>, DocA0),
+    DocB = couch_util:get_value(<<"ok">>, DocB0),
+
+    ?_assertEqual([RevA, RevB], [DocA, DocB]).
+
+
+should_validate_doc_with_bad_id(Pid) ->
+    DocId = <<"_docudoc">>,
+
+    Req = fake_request(DocId),
+    chttpd_db:db_req(Req, nil),
+
+    [{Result}] = get_results_from_response(Pid),
+    ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)),
+
+    Docs = couch_util:get_value(<<"docs">>, Result),
+    ?assertEqual(1, length(Docs)),
+    [{DocResult}] = Docs,
+
+    Doc = couch_util:get_value(<<"error">>, DocResult),
+
+    ?_assertMatch({[{<<"id">>, DocId},
+                    {<<"rev">>, null},
+                    {<<"error">>, <<"bad_request">>},
+                    {<<"reason">>, _}]},
+                  Doc).
+
+
+should_validate_doc_with_bad_rev(Pid) ->
+    DocId = <<"docudoc">>,
+    Rev = <<"revorev">>,
+
+    Req = fake_request(DocId, Rev),
+    chttpd_db:db_req(Req, nil),
+
+    [{Result}] = get_results_from_response(Pid),
+    ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)),
+
+    Docs = couch_util:get_value(<<"docs">>, Result),
+    ?assertEqual(1, length(Docs)),
+    [{DocResult}] = Docs,
+
+    Doc = couch_util:get_value(<<"error">>, DocResult),
+
+    ?_assertMatch({[{<<"id">>, DocId},
+                    {<<"rev">>, Rev},
+                    {<<"error">>, <<"bad_request">>},
+                    {<<"reason">>, _}]},
+                  Doc).
+
+
+should_validate_missing_doc(Pid) ->
+    DocId = <<"docudoc">>,
+    Rev = <<"1-revorev">>,
+
+    Req = fake_request(DocId, Rev),
+    mock_open_revs([{1,<<"revorev">>}], {ok, []}),
+    chttpd_db:db_req(Req, nil),
+
+    [{Result}] = get_results_from_response(Pid),
+    ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)),
+
+    Docs = couch_util:get_value(<<"docs">>, Result),
+    ?assertEqual(1, length(Docs)),
+    [{DocResult}] = Docs,
+
+    Doc = couch_util:get_value(<<"error">>, DocResult),
+
+    ?_assertMatch({[{<<"id">>, DocId},
+                    {<<"rev">>, Rev},
+                    {<<"error">>, <<"not_found">>},
+                    {<<"reason">>, _}]},
+                  Doc).
+
+
+should_validate_bad_atts_since(Pid) ->
+    DocId = <<"docudoc">>,
+    Rev = <<"1-revorev">>,
+
+    Req = fake_request(DocId, Rev, <<"badattsince">>),
+    mock_open_revs([{1,<<"revorev">>}], {ok, []}),
+    chttpd_db:db_req(Req, nil),
+
+    [{Result}] = get_results_from_response(Pid),
+    ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)),
+
+    Docs = couch_util:get_value(<<"docs">>, Result),
+    ?assertEqual(1, length(Docs)),
+    [{DocResult}] = Docs,
+
+    Doc = couch_util:get_value(<<"error">>, DocResult),
+
+    ?_assertMatch({[{<<"id">>, DocId},
+                    {<<"rev">>, <<"badattsince">>},
+                    {<<"error">>, <<"bad_request">>},
+                    {<<"reason">>, _}]},
+                  Doc).
+
+
+should_include_attachments_when_atts_since_specified(_) ->
+    DocId = <<"docudoc">>,
+    Rev = <<"1-revorev">>,
+
+    Req = fake_request(DocId, Rev, [<<"1-abc">>]),
+    mock_open_revs([{1,<<"revorev">>}], {ok, []}),
+    chttpd_db:db_req(Req, nil),
+
+    ?_assert(meck:called(fabric, open_revs,
+                         [nil, DocId, [{1, <<"revorev">>}],
+                          [{atts_since, [{1, <<"abc">>}]}, attachments]])).
+
+%% helpers
+
+fake_request(Payload) when is_tuple(Payload) ->
+    #httpd{method='POST', path_parts=[<<"db">>, <<"_bulk_get">>],
+           mochi_req=mochireq, req_body=Payload};
+fake_request(DocId) when is_binary(DocId) ->
+    fake_request({[{<<"docs">>, [{[{<<"id">>, DocId}]}]}]}).
+
+fake_request(DocId, Rev) ->
+    fake_request({[{<<"docs">>, [{[{<<"id">>, DocId}, {<<"rev">>, Rev}]}]}]}).
+
+fake_request(DocId, Rev, AttsSince) ->
+    fake_request({[{<<"docs">>, [{[{<<"id">>, DocId},
+                                   {<<"rev">>, Rev},
+                                   {<<"atts_since">>, AttsSince}]}]}]}).
+
+
+mock_open_revs(RevsReq0, RevsResp) ->
+    ok = meck:expect(fabric, open_revs,
+                     fun(_, _, RevsReq1, _) ->
+                         ?assertEqual(RevsReq0, RevsReq1),
+                         RevsResp
+                     end).
+
+
+mock(mochireq) ->
+    ok = meck:new(mochireq, [non_strict]),
+    ok = meck:expect(mochireq, parse_qs, fun() -> [] end),
+    ok = meck:expect(mochireq, accepts_content_type, fun(_) -> false end),
+    ok;
+mock(couch_httpd) ->
+    ok = meck:new(couch_httpd, [passthrough]),
+    ok = meck:expect(couch_httpd, validate_ctype, fun(_, _) -> ok end),
+    ok;
+mock(chttpd) ->
+    ok = meck:new(chttpd, [passthrough]),
+    ok = meck:expect(chttpd, start_json_response, fun(_, _) -> {ok, nil} end),
+    ok = meck:expect(chttpd, end_json_response, fun(_) -> ok end),
+    ok = meck:expect(chttpd, send_chunk, fun send_chunk/2),
+    ok = meck:expect(chttpd, json_body_obj, fun (#httpd{req_body=Body}) -> Body end),
+    ok;
+mock(couch_epi) ->
+    ok = meck:new(couch_epi, [passthrough]),
+    ok = meck:expect(couch_epi, any, fun(_, _, _, _, _) -> false end),
+    ok;
+mock(couch_stats) ->
+    ok = meck:new(couch_stats, [passthrough]),
+    ok = meck:expect(couch_stats, increment_counter, fun(_) -> ok end),
+    ok = meck:expect(couch_stats, increment_counter, fun(_, _) -> ok end),
+    ok = meck:expect(couch_stats, decrement_counter, fun(_) -> ok end),
+    ok = meck:expect(couch_stats, decrement_counter, fun(_, _) -> ok end),
+    ok = meck:expect(couch_stats, update_histogram, fun(_, _) -> ok end),
+    ok = meck:expect(couch_stats, update_gauge, fun(_, _) -> ok end),
+    ok;
+mock(fabric) ->
+    ok = meck:new(fabric, [passthrough]),
+    ok.
+
+
+spawn_accumulator() ->
+    Parent = self(),
+    Pid = spawn(fun() -> accumulator_loop(Parent, []) end),
+    erlang:put(chunks_gather, Pid),
+    Pid.
+
+accumulator_loop(Parent, Acc) ->
+    receive
+        {stop, Ref} ->
+            Parent ! {ok, Ref};
+        {get, Ref} ->
+            Parent ! {ok, Ref, Acc},
+            accumulator_loop(Parent, Acc);
+        {put, Ref, Chunk} ->
+            Parent ! {ok, Ref},
+            accumulator_loop(Parent, [Chunk|Acc])
+    end.
+
+stop_accumulator(Pid) ->
+    Ref = make_ref(),
+    Pid ! {stop, Ref},
+    receive
+        {ok, Ref} ->
+            ok
+    after ?TIMEOUT ->
+        throw({timeout, <<"process stop timeout">>})
+    end.
+
+
+send_chunk(_, []) ->
+    {ok, nil};
+send_chunk(_Req, [H|T]=Chunk) when is_list(Chunk) ->
+    send_chunk(_Req, H),
+    send_chunk(_Req, T);
+send_chunk(_, Chunk) ->
+    Worker = erlang:get(chunks_gather),
+    Ref = make_ref(),
+    Worker ! {put, Ref, Chunk},
+    receive
+        {ok, Ref} -> {ok, nil}
+    after ?TIMEOUT ->
+        throw({timeout, <<"send chunk timeout">>})
+    end.
+
+
+get_response(Pid) ->
+    Ref = make_ref(),
+    Pid ! {get, Ref},
+    receive
+        {ok, Ref, Acc} ->
+            ?JSON_DECODE(iolist_to_binary(lists:reverse(Acc)))
+    after ?TIMEOUT ->
+        throw({timeout, <<"get response timeout">>})
+    end.
+
+
+get_results_from_response(Pid) ->
+    {Resp} = get_response(Pid),
+    couch_util:get_value(<<"results">>, Resp).