Skip to content

Commit 881f52f

Browse files
committed
Add option to delay responses until the end
When set, every response is sent once fully generated on the server side. This increases memory usage on the nodes but simplifies error handling for the client as it eliminates the possibility that the response will be deliberately terminated midway through due to a timeout. The config value can be changed at runtime without impacting any in-flight responses.
1 parent e7822a5 commit 881f52f

File tree

3 files changed

+125
-9
lines changed

3 files changed

+125
-9
lines changed

rel/overlay/etc/default.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ prefer_minimal = Cache-Control, Content-Length, Content-Range, Content-Type, ETa
130130
; _dbs_info in a request
131131
max_db_number_for_dbs_info_req = 100
132132

133+
; set to true to delay the start of a response until the end has been calculated
134+
;buffer_response = false
135+
133136
; authentication handlers
134137
; authentication_handlers = {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
135138
; uncomment the next line to enable proxy authentication

src/chttpd/src/chttpd.erl

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@
5252
req,
5353
code,
5454
headers,
55-
first_chunk,
56-
resp=nil
55+
chunks,
56+
resp=nil,
57+
buffer_response=false
5758
}).
5859

5960
start_link() ->
@@ -780,40 +781,54 @@ start_json_response(Req, Code, Headers0) ->
780781
end_json_response(Resp) ->
781782
couch_httpd:end_json_response(Resp).
782783

784+
783785
start_delayed_json_response(Req, Code) ->
784786
start_delayed_json_response(Req, Code, []).
785787

788+
786789
start_delayed_json_response(Req, Code, Headers) ->
787790
start_delayed_json_response(Req, Code, Headers, "").
788791

792+
789793
start_delayed_json_response(Req, Code, Headers, FirstChunk) ->
790794
{ok, #delayed_resp{
791795
start_fun = fun start_json_response/3,
792796
req = Req,
793797
code = Code,
794798
headers = Headers,
795-
first_chunk = FirstChunk}}.
799+
chunks = [FirstChunk],
800+
buffer_response = buffer_response(Req)}}.
801+
796802

797803
start_delayed_chunked_response(Req, Code, Headers) ->
798804
start_delayed_chunked_response(Req, Code, Headers, "").
799805

806+
800807
start_delayed_chunked_response(Req, Code, Headers, FirstChunk) ->
801808
{ok, #delayed_resp{
802809
start_fun = fun start_chunked_response/3,
803810
req = Req,
804811
code = Code,
805812
headers = Headers,
806-
first_chunk = FirstChunk}}.
813+
chunks = [FirstChunk],
814+
buffer_response = buffer_response(Req)}}.
815+
807816

808-
send_delayed_chunk(#delayed_resp{}=DelayedResp, Chunk) ->
817+
send_delayed_chunk(#delayed_resp{buffer_response=false}=DelayedResp, Chunk) ->
809818
{ok, #delayed_resp{resp=Resp}=DelayedResp1} =
810819
start_delayed_response(DelayedResp),
811820
{ok, Resp} = send_chunk(Resp, Chunk),
812-
{ok, DelayedResp1}.
821+
{ok, DelayedResp1};
822+
823+
send_delayed_chunk(#delayed_resp{buffer_response=true}=DelayedResp, Chunk) ->
824+
#delayed_resp{chunks = Chunks} = DelayedResp,
825+
{ok, DelayedResp#delayed_resp{chunks = [Chunk | Chunks]}}.
826+
813827

814828
send_delayed_last_chunk(Req) ->
815829
send_delayed_chunk(Req, []).
816830

831+
817832
send_delayed_error(#delayed_resp{req=Req,resp=nil}=DelayedResp, Reason) ->
818833
{Code, ErrorStr, ReasonStr} = error_info(Reason),
819834
{ok, Resp} = send_error(Req, Code, ErrorStr, ReasonStr),
@@ -823,6 +838,7 @@ send_delayed_error(#delayed_resp{resp=Resp, req=Req}, Reason) ->
823838
log_error_with_stack_trace(Reason),
824839
throw({http_abort, Resp, Reason}).
825840

841+
826842
close_delayed_json_object(Resp, Buffer, Terminator, 0) ->
827843
% Use a separate chunk to close the streamed array to maintain strict
828844
% compatibility with earlier versions. See COUCHDB-2724
@@ -831,10 +847,22 @@ close_delayed_json_object(Resp, Buffer, Terminator, 0) ->
831847
close_delayed_json_object(Resp, Buffer, Terminator, _Threshold) ->
832848
send_delayed_chunk(Resp, [Buffer | Terminator]).
833849

834-
end_delayed_json_response(#delayed_resp{}=DelayedResp) ->
850+
851+
end_delayed_json_response(#delayed_resp{buffer_response=false}=DelayedResp) ->
835852
{ok, #delayed_resp{resp=Resp}} =
836853
start_delayed_response(DelayedResp),
837-
end_json_response(Resp).
854+
end_json_response(Resp);
855+
856+
end_delayed_json_response(#delayed_resp{buffer_response=true}=DelayedResp) ->
857+
#delayed_resp{
858+
req = Req,
859+
code = Code,
860+
headers = Headers,
861+
chunks = Chunks
862+
} = DelayedResp,
863+
{ok, Resp} = start_response_length(Req, Code, Headers, iolist_size(Chunks)),
864+
send(Resp, lists:reverse(Chunks)).
865+
838866

839867
get_delayed_req(#delayed_resp{req=#httpd{mochi_req=MochiReq}}) ->
840868
MochiReq;
@@ -847,7 +875,7 @@ start_delayed_response(#delayed_resp{resp=nil}=DelayedResp) ->
847875
req=Req,
848876
code=Code,
849877
headers=Headers,
850-
first_chunk=FirstChunk
878+
chunks=[FirstChunk]
851879
}=DelayedResp,
852880
{ok, Resp} = StartFun(Req, Code, Headers),
853881
case FirstChunk of
@@ -858,6 +886,18 @@ start_delayed_response(#delayed_resp{resp=nil}=DelayedResp) ->
858886
start_delayed_response(#delayed_resp{}=DelayedResp) ->
859887
{ok, DelayedResp}.
860888

889+
890+
buffer_response(Req) ->
891+
case chttpd:qs_value(Req, "buffer_response") of
892+
"false" ->
893+
false;
894+
"true" ->
895+
true;
896+
_ ->
897+
config:get_boolean("chttpd", "buffer_response", false)
898+
end.
899+
900+
861901
error_info({Error, Reason}) when is_list(Reason) ->
862902
error_info({Error, couch_util:to_binary(Reason)});
863903
error_info(bad_request) ->
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
-module(chttpd_delayed_test).
2+
3+
-include_lib("couch/include/couch_eunit.hrl").
4+
-include_lib("couch/include/couch_db.hrl").
5+
6+
-define(USER, "chttpd_view_test_admin").
7+
-define(PASS, "pass").
8+
-define(AUTH, {basic_auth, {?USER, ?PASS}}).
9+
-define(CONTENT_JSON, {"Content-Type", "application/json"}).
10+
-define(DDOC, "{\"_id\": \"_design/bar\", \"views\": {\"baz\":
11+
{\"map\": \"function(doc) {emit(doc._id, doc._id);}\"}}}").
12+
13+
-define(FIXTURE_TXT, ?ABS_PATH(?FILE)).
14+
-define(i2l(I), integer_to_list(I)).
15+
-define(TIMEOUT, 60). % seconds
16+
17+
setup() ->
18+
Hashed = couch_passwords:hash_admin_password(?PASS),
19+
ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
20+
ok = config:set("chttpd", "buffer_response", "true"),
21+
TmpDb = ?tempdb(),
22+
Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
23+
Port = mochiweb_socket_server:get(chttpd, port),
24+
Url = lists:concat(["http://", Addr, ":", Port, "/", ?b2l(TmpDb)]),
25+
create_db(Url),
26+
Url.
27+
28+
teardown(Url) ->
29+
delete_db(Url),
30+
ok = config:delete("admins", ?USER, _Persist=false).
31+
32+
create_db(Url) ->
33+
{ok, Status, _, _} = test_request:put(Url, [?CONTENT_JSON, ?AUTH], "{}"),
34+
?assert(Status =:= 201 orelse Status =:= 202).
35+
36+
37+
delete_db(Url) ->
38+
{ok, 200, _, _} = test_request:delete(Url, [?AUTH]).
39+
40+
41+
all_test_() ->
42+
{
43+
"chttpd delay tests",
44+
{
45+
setup,
46+
fun chttpd_test_util:start_couch/0, fun chttpd_test_util:stop_couch/1,
47+
{
48+
foreach,
49+
fun setup/0, fun teardown/1,
50+
[
51+
fun test_buffer_response_all_docs/1,
52+
fun test_buffer_response_changes/1
53+
]
54+
}
55+
}
56+
}.
57+
58+
59+
test_buffer_response_all_docs(Url) ->
60+
assert_has_content_length(Url ++ "/_all_docs").
61+
62+
63+
test_buffer_response_changes(Url) ->
64+
assert_has_content_length(Url ++ "/_changes").
65+
66+
67+
assert_has_content_length(Url) ->
68+
{timeout, ?TIMEOUT, ?_test(begin
69+
{ok, Code, Headers, _Body} = test_request:get(Url, [?AUTH]),
70+
?assertEqual(200, Code),
71+
?assert(lists:keymember("Content-Length", 1, Headers))
72+
end)}.
73+

0 commit comments

Comments
 (0)