Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Changing mechanism of the load balancer

Whenever a server would listen to TCP connections but never accept
them, the lhttpc application would leak a ton of processes until
the virtual machine is taken down.

This was due to the way setting up connections was being done
within the load balancer. This would lead to many milliseconds of
delay for each socket connection attempt, and an eventual queue
build-up would happen in the load balancer.
Because requests freely spawn processes, this ended up having
too many requests that the LB cannot deal with.

This fix changes the structure around so that each client is
responsible of setting their own socket and connection, enabling
the load balancer to easily deny connections to newer processes
when older ones are still stuck. Setting a good request timeout
can then insure that slow requests won't starve the system.
  • Loading branch information...
commit 68ab97c7096f422ac29dd5773a4681c3643d0b84 1 parent bbecd1d
Fred Hebert authored
1  rebar.config
... ... @@ -1 +1,2 @@
  1 +{erl_opts, [debug_info]}.
1 2 {cover_enabled, true}.
10 src/lhttpc.app.src
@@ -29,11 +29,11 @@
29 29 %%% @end
30 30 {application, lhttpc,
31 31 [{description, "Lightweight HTTP Client"},
32   - {vsn, "1.2.6"},
33   - {modules, []},
34   - {registered, [lhttpc_manager]},
  32 + {vsn, "1.2.7"},
  33 + {modules, [lhttpc,lhttpc_sup,lhttpc_client,lhttpc_sock,lhttp_lb]},
  34 + {registered, [lhttpc_sup]},
35 35 {applications, [kernel, stdlib, ssl, crypto]},
36   - {mod, {lhttpc, nil}},
37   - {env, []}
  36 + {mod, {lhttpc, []}},
  37 + {env, [{connection_timeout, 300000}]}
38 38 ]}.
39 39
50 src/lhttpc.erl
@@ -52,7 +52,7 @@
52 52 %% @hidden
53 53 -spec start(normal | {takeover, node()} | {failover, node()}, any()) ->
54 54 {ok, pid()}.
55   -start(_, _) ->
  55 +start(_, Opts) ->
56 56 case lists:member({seed,1}, ssl:module_info(exports)) of
57 57 true ->
58 58 % Make sure that the ssl random number generator is seeded
@@ -61,7 +61,9 @@ start(_, _) ->
61 61 false ->
62 62 ok
63 63 end,
64   - lhttpc_sup:start_link().
  64 + if is_list(Opts) -> lhttpc_sup:start_link(Opts);
  65 + true -> lhttpc_sup:start_link()
  66 + end.
65 67
66 68 %% @hidden
67 69 -spec stop(any()) -> ok.
@@ -320,12 +322,16 @@ request(URL, Method, Hdrs, Body, Timeout, Options) ->
320 322 headers(), iolist(), pos_integer(), [option()]) -> result().
321 323 request(Host, Port, Ssl, Path, Method, Hdrs, Body, Timeout, Options) ->
322 324 verify_options(Options, []),
323   - ReqId = erlang:now(),
  325 + ReqId = now(),
324 326 case proplists:is_defined(stream_to, Options) of
325 327 true ->
326 328 StreamTo = proplists:get_value(stream_to, Options),
327 329 Args = [ReqId, StreamTo, Host, Port, Ssl, Path, Method, Hdrs, Body, Options],
328   - Pid = spawn(lhttpc_client, request_with_timeout, [Timeout, Args]),
  330 + Pid = spawn(lhttpc_client, request, Args),
  331 + spawn(fun() ->
  332 + R = kill_client_after(Pid, Timeout),
  333 + StreamTo ! {response, ReqId, Pid, R}
  334 + end),
329 335 {ReqId, Pid};
330 336 false ->
331 337 Args = [ReqId, self(), Host, Port, Ssl, Path, Method, Hdrs, Body, Options],
@@ -338,7 +344,7 @@ request(Host, Port, Ssl, Path, Method, Hdrs, Body, Timeout, Options) ->
338 344 % linked client send us an exit signal, since this can be
339 345 % caught by the caller.
340 346 exit(Reason);
341   - {'EXIT', ReqId, Pid, Reason} ->
  347 + {'EXIT', Pid, Reason} ->
342 348 % This could happen if the process we're running in taps exits
343 349 % and the client process exits due to some exit signal being
344 350 % sent to it. Very unlikely though
@@ -397,7 +403,7 @@ send_body_part({Pid, 0}, IoList, Timeout) when is_pid(Pid) ->
397 403 send_body_part({Pid, 1}, IoList, Timeout);
398 404 {response, _ReqId, Pid, R} ->
399 405 R;
400   - {exit, Pid, Reason} ->
  406 + {exit, _ReqId, Pid, Reason} ->
401 407 exit(Reason);
402 408 {'EXIT', Pid, Reason} ->
403 409 exit(Reason)
@@ -410,9 +416,9 @@ send_body_part({Pid, Window}, IoList, _Timeout) when Window > 0, is_pid(Pid) ->
410 416 receive
411 417 {ack, Pid} ->
412 418 {ok, {Pid, Window}};
413   - {response, _ReqId, Pid, R} ->
  419 + {reponse, _ReqId, Pid, R} ->
414 420 R;
415   - {exit, Pid, Reason} ->
  421 + {exit, _ReqId, Pid, Reason} ->
416 422 exit(Reason);
417 423 {'EXIT', Pid, Reason} ->
418 424 exit(Reason)
@@ -515,7 +521,7 @@ read_response(Pid, Timeout) ->
515 521 read_response(Pid, Timeout);
516 522 {response, _ReqId, Pid, R} ->
517 523 R;
518   - {exit, Pid, Reason} ->
  524 + {exit, _ReqId, Pid, Reason} ->
519 525 exit(Reason);
520 526 {'EXIT', Pid, Reason} ->
521 527 exit(Reason)
@@ -537,6 +543,23 @@ kill_client(Pid) ->
537 543 erlang:error(Reason)
538 544 end.
539 545
  546 +kill_client_after(Pid, Timeout) ->
  547 + erlang:monitor(process, Pid),
  548 + receive
  549 + {'DOWN', _, process, Pid, _Reason} -> exit(normal)
  550 + after Timeout ->
  551 + catch unlink(Pid), % or we'll kill ourself :O
  552 + exit(Pid, timeout),
  553 + receive
  554 + {'DOWN', _, process, Pid, timeout} ->
  555 + {error, timeout};
  556 + {'DOWN', _, process, Pid, Reason} ->
  557 + erlang:error(Reason)
  558 + after 1000 ->
  559 + exit(normal) % silent failure!
  560 + end
  561 + end.
  562 +
540 563 -spec verify_options(options(), options()) -> ok.
541 564 verify_options([{send_retry, N} | Options], Errors)
542 565 when is_integer(N), N >= 0 ->
@@ -551,11 +574,8 @@ verify_options([{connection_timeout, infinity} | Options], Errors) ->
551 574 verify_options([{connection_timeout, MS} | Options], Errors)
552 575 when is_integer(MS), MS >= 0 ->
553 576 verify_options(Options, Errors);
554   -verify_options([{max_connections, MS} | Options], Errors)
555   - when is_integer(MS), MS >= 0 ->
556   - verify_options(Options, Errors);
557   -verify_options([{stream_to, Pid} | Options], Errors)
558   - when is_pid(Pid) ->
  577 +verify_options([{max_connections, N} | Options], Errors)
  578 + when is_integer(N), N > 0 ->
559 579 verify_options(Options, Errors);
560 580 verify_options([{partial_upload, WindowSize} | Options], Errors)
561 581 when is_integer(WindowSize), WindowSize >= 0 ->
@@ -574,6 +594,8 @@ verify_options([{partial_download, DownloadOptions} | Options], Errors)
574 594 verify_options([{connect_options, List} | Options], Errors)
575 595 when is_list(List) ->
576 596 verify_options(Options, Errors);
  597 +verify_options([{stream_to, Pid} | Options], Errors) when is_pid(Pid) ->
  598 + verify_options(Options, Errors);
577 599 verify_options([Option | Options], Errors) ->
578 600 verify_options(Options, [Option | Errors]);
579 601 verify_options([], []) ->
84 src/lhttpc_client.erl
@@ -29,22 +29,20 @@
29 29 %%% @doc
30 30 %%% This module implements the HTTP request handling. This should normally
31 31 %%% not be called directly since it should be spawned by the lhttpc module.
32   -%%% @end
33 32 -module(lhttpc_client).
34 33
35   --export([request/10, request_with_timeout/2]).
  34 +-export([request/10]).
36 35
37 36 -include("lhttpc_types.hrl").
38 37
39 38 -record(client_state, {
40   - req_id :: tuple(),
  39 + req_id :: term(),
41 40 host :: string(),
42 41 port = 80 :: integer(),
43 42 ssl = false :: true | false,
44 43 method :: string(),
45 44 request :: iolist(),
46 45 request_headers :: headers(),
47   - load_balancer:: pid(),
48 46 socket,
49 47 connect_timeout = infinity :: timeout(),
50 48 connect_options = [] :: [any()],
@@ -63,15 +61,9 @@
63 61 -define(CONNECTION_HDR(HDRS, DEFAULT),
64 62 string:to_lower(lhttpc_lib:header_value("connection", HDRS, DEFAULT))).
65 63
66   -request_with_timeout(Timeout, [ReqId, StreamTo, _Host, _Port, _Ssl, _Path, _Method, _Hdrs, _Body, _Options] = Args) ->
67   - TimerRef = erlang:send_after(Timeout, lhttpc_manager, {kill_client_after_timeout, ReqId, self(), StreamTo}),
68   - ok = apply(?MODULE, request, Args),
69   - erlang:cancel_timer(TimerRef).
70   -
71   --spec request(tuple(), pid(), string(), 1..65535, true | false, string(),
  64 +-spec request(term(), pid(), string(), 1..65535, true | false, string(),
72 65 string() | atom(), headers(), iolist(), [option()]) -> no_return().
73   -%% @spec (ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, RequestBody, Options) -> ok
74   -%% ReqId = tuple()
  66 +%% @spec (From, Host, Port, Ssl, Path, Method, Hdrs, RequestBody, Options) -> ok
75 67 %% From = pid()
76 68 %% Host = string()
77 69 %% Port = integer()
@@ -108,14 +100,16 @@ execute(ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) ->
108 100 PartialUpload = proplists:is_defined(partial_upload, Options),
109 101 PartialDownload = proplists:is_defined(partial_download, Options),
110 102 PartialDownloadOptions = proplists:get_value(partial_download, Options, []),
111   - ConnectOptions = proplists:get_value(connect_options, Options, []),
112 103 NormalizedMethod = lhttpc_lib:normalize_method(Method),
113 104 MaxConnections = proplists:get_value(max_connections, Options, 10),
114 105 ConnectionTimeout = proplists:get_value(connection_timeout, Options, infinity),
115 106 {ChunkedUpload, Request} = lhttpc_lib:format_request(Path, NormalizedMethod,
116 107 Hdrs, Host, Port, Body, PartialUpload),
117   - LbRequest = {lb, Host, Port, Ssl, MaxConnections, ConnectionTimeout},
118   - {ok, Lb} = gen_server:call(lhttpc_manager, LbRequest, infinity),
  108 + Socket = case lhttpc_lb:checkout(Host, Port, Ssl, MaxConnections, ConnectionTimeout) of
  109 + {ok, S} -> S; % Re-using HTTP/1.1 connections
  110 + retry_later -> throw(retry_later);
  111 + no_socket -> undefined % Opening a new HTTP/1.1 connection
  112 + end,
119 113 State = #client_state{
120 114 req_id = ReqId,
121 115 host = Host,
@@ -125,10 +119,10 @@ execute(ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) ->
125 119 request = Request,
126 120 requester = From,
127 121 request_headers = Hdrs,
128   - load_balancer = Lb,
  122 + socket = Socket,
129 123 connect_timeout = proplists:get_value(connect_timeout, Options,
130 124 infinity),
131   - connect_options = ConnectOptions,
  125 + connect_options = proplists:get_value(connect_options, Options, []),
132 126 attempts = 1 + proplists:get_value(send_retry, Options, 1),
133 127 partial_upload = PartialUpload,
134 128 upload_window = UploadWindowSize,
@@ -143,56 +137,59 @@ execute(ReqId, From, Host, Port, Ssl, Path, Method, Hdrs, Body, Options) ->
143 137 {R, undefined} ->
144 138 {ok, R};
145 139 {R, NewSocket} ->
146   - case lhttpc_sock:controlling_process(NewSocket, Lb, Ssl) of
147   - ok ->
148   - gen_server:cast(Lb, {store, NewSocket});
149   - _ ->
150   - ok
151   - end,
  140 + % The socket we ended up doing the request over is returned
  141 + % here, it might be the same as Socket, but we don't know.
  142 + lhttpc_lb:checkin(Host, Port, Ssl, NewSocket),
152 143 {ok, R}
153 144 end,
154 145 {response, ReqId, self(), Response}.
155 146
156 147 send_request(#client_state{attempts = 0}) ->
  148 + % Don't try again if the number of allowed attempts is 0.
157 149 throw(connection_closed);
158 150 send_request(#client_state{socket = undefined} = State) ->
  151 + Host = State#client_state.host,
  152 + Port = State#client_state.port,
  153 + Ssl = State#client_state.ssl,
  154 + Timeout = State#client_state.connect_timeout,
159 155 ConnectOptions = State#client_state.connect_options,
160   - ConnectTimeout = State#client_state.connect_timeout,
161   - Lb = State#client_state.load_balancer,
162   - SocketRequest = {socket, self(), ConnectOptions, ConnectTimeout},
163   - case gen_server:call(Lb, SocketRequest, infinity) of
  156 + SocketOptions = [binary, {packet, http}, {active, false} | ConnectOptions],
  157 + case lhttpc_sock:connect(Host, Port, SocketOptions, Timeout, Ssl) of
164 158 {ok, Socket} ->
165   - lhttpc_sock:setopts(Socket, [{active, false}], State#client_state.ssl),
166 159 send_request(State#client_state{socket = Socket});
  160 + {error, etimedout} ->
  161 + % TCP stack decided to give up
  162 + throw(connect_timeout);
  163 + {error, timeout} ->
  164 + throw(connect_timeout);
167 165 {error, Reason} ->
168   - throw(Reason)
  166 + erlang:error(Reason)
169 167 end;
170 168 send_request(State) ->
171   - Lb = State#client_state.load_balancer,
172 169 Socket = State#client_state.socket,
173 170 Ssl = State#client_state.ssl,
174 171 Request = State#client_state.request,
175 172 case lhttpc_sock:send(Socket, Request, Ssl) of
176 173 ok ->
177 174 if
178   - State#client_state.partial_upload -> partial_upload(State);
  175 + State#client_state.partial_upload -> partial_upload(State);
179 176 not State#client_state.partial_upload -> read_response(State)
180 177 end;
181 178 {error, closed} ->
182   - gen_server:cast(Lb, {remove, Socket}),
  179 + lhttpc_sock:close(Socket, Ssl),
183 180 NewState = State#client_state{
184 181 socket = undefined,
185 182 attempts = State#client_state.attempts - 1
186 183 },
187 184 send_request(NewState);
188 185 {error, Reason} ->
189   - gen_server:cast(Lb, {remove, Socket}),
  186 + lhttpc_sock:close(Socket, Ssl),
190 187 erlang:error(Reason)
191 188 end.
192 189
193 190 partial_upload(State) ->
194 191 Response = {ok, {self(), State#client_state.upload_window}},
195   - State#client_state.requester ! {response, State#client_state.req_id, self(), Response},
  192 + State#client_state.requester ! {response,State#client_state.req_id, self(), Response},
196 193 partial_upload_loop(State#client_state{attempts = 1, request = undefined}).
197 194
198 195 partial_upload_loop(State = #client_state{requester = Pid}) ->
@@ -233,8 +230,8 @@ encode_body_part(#client_state{chunked_upload = false}, Data) ->
233 230
234 231 check_send_result(_State, ok) ->
235 232 ok;
236   -check_send_result(#client_state{socket = Socket, load_balancer = Lb}, {error, Reason}) ->
237   - gen_server:cast(Lb, {remove, Socket}),
  233 +check_send_result(#client_state{socket = Sock, ssl = Ssl}, {error, Reason}) ->
  234 + lhttpc_sock:close(Sock, Ssl),
238 235 throw(Reason).
239 236
240 237 read_response(#client_state{socket = Socket, ssl = Ssl} = State) ->
@@ -242,7 +239,6 @@ read_response(#client_state{socket = Socket, ssl = Ssl} = State) ->
242 239 read_response(State, nil, {nil, nil}, []).
243 240
244 241 read_response(State, Vsn, {StatusCode, _} = Status, Hdrs) ->
245   - Lb = State#client_state.load_balancer,
246 242 Socket = State#client_state.socket,
247 243 Ssl = State#client_state.ssl,
248 244 case lhttpc_sock:recv(Socket, Ssl) of
@@ -265,7 +261,7 @@ read_response(State, Vsn, {StatusCode, _} = Status, Hdrs) ->
265 261 Response = handle_response_body(State, Vsn, Status, Hdrs),
266 262 NewHdrs = element(2, Response),
267 263 ReqHdrs = State#client_state.request_headers,
268   - NewSocket = maybe_close_socket(Lb, Socket, Vsn, ReqHdrs, NewHdrs),
  264 + NewSocket = maybe_close_socket(Socket, Ssl, Vsn, ReqHdrs, NewHdrs),
269 265 {Response, NewSocket};
270 266 {error, closed} ->
271 267 % Either we only noticed that the socket was closed after we
@@ -273,14 +269,14 @@ read_response(State, Vsn, {StatusCode, _} = Status, Hdrs) ->
273 269 % the request on the wire or the server has some issues and is
274 270 % closing connections without sending responses.
275 271 % If this the first attempt to send the request, we will try again.
276   - gen_server:cast(Lb, {remove, Socket}),
  272 + lhttpc_sock:close(Socket, Ssl),
277 273 NewState = State#client_state{
278 274 socket = undefined,
279 275 attempts = State#client_state.attempts - 1
280 276 },
281 277 send_request(NewState);
282 278 {error, timeout} ->
283   - gen_server:cast(Lb, {remove, Socket}),
  279 + lhttpc_sock:close(Socket, Ssl),
284 280 NewState = State#client_state{
285 281 socket = undefined,
286 282 attempts = 0
@@ -622,22 +618,22 @@ read_until_closed(Socket, Acc, Hdrs, Ssl) ->
622 618 erlang:error(Reason)
623 619 end.
624 620
625   -maybe_close_socket(Lb, Socket, {1, Minor}, ReqHdrs, RespHdrs) when Minor >= 1->
  621 +maybe_close_socket(Socket, Ssl, {1, Minor}, ReqHdrs, RespHdrs) when Minor >= 1->
626 622 ClientConnection = ?CONNECTION_HDR(ReqHdrs, "keep-alive"),
627 623 ServerConnection = ?CONNECTION_HDR(RespHdrs, "keep-alive"),
628 624 if
629 625 ClientConnection =:= "close"; ServerConnection =:= "close" ->
630   - gen_server:cast(Lb, {remove, Socket}),
  626 + lhttpc_sock:close(Socket, Ssl),
631 627 undefined;
632 628 ClientConnection =/= "close", ServerConnection =/= "close" ->
633 629 Socket
634 630 end;
635   -maybe_close_socket(Lb, Socket, _, ReqHdrs, RespHdrs) ->
  631 +maybe_close_socket(Socket, Ssl, _, ReqHdrs, RespHdrs) ->
636 632 ClientConnection = ?CONNECTION_HDR(ReqHdrs, "keep-alive"),
637 633 ServerConnection = ?CONNECTION_HDR(RespHdrs, "close"),
638 634 if
639 635 ClientConnection =:= "close"; ServerConnection =/= "keep-alive" ->
640   - gen_server:cast(Lb, {remove, Socket}),
  636 + lhttpc_sock:close(Socket, Ssl),
641 637 undefined;
642 638 ClientConnection =/= "close", ServerConnection =:= "keep-alive" ->
643 639 Socket
387 src/lhttpc_lb.erl
... ... @@ -1,195 +1,250 @@
  1 +%%% Load balancer for lhttpc, replacing the older lhttpc_manager.
  2 +%%% Takes a similar stance of storing used-but-not-closed sockets.
  3 +%%% Also adds functionality to limit the number of simultaneous
  4 +%%% connection attempts from clients.
1 5 -module(lhttpc_lb).
  6 +-behaviour(gen_server).
  7 +-export([start_link/5, checkout/5, checkin/3, checkin/4]).
  8 +-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
  9 + code_change/3, terminate/2]).
2 10
3   --export([
4   - start_link/1
5   - ]).
6   --export([
7   - init/1,
8   - handle_call/3,
9   - handle_cast/2,
10   - handle_info/2,
11   - code_change/3,
12   - terminate/2
13   - ]).
  11 +-define(SHUTDOWN_DELAY, 10000).
14 12
15   --behaviour(gen_server).
  13 +%% TODO: transfert_socket, in case of checkout+give_away
16 14
17   --record(httpc_man, {
18   - host :: string(),
19   - port = 80 :: integer(),
20   - ssl = false :: true | false,
21   - max_connections = 10 :: non_neg_integer(),
22   - connection_timeout = 300000 :: non_neg_integer(),
23   - sockets,
24   - available_sockets = []
25   - }).
26   -
27   -%% @spec (any()) -> {ok, pid()}
28   -%% @doc Starts and link to the gen server.
29   -%% This is normally called by a supervisor.
30   -%% @end
31   --spec start_link(any()) -> {ok, pid()}.
32   -start_link([Dest, Opts]) ->
33   - gen_server:start_link(?MODULE, [Dest, Opts], []).
34   -
35   -%% @hidden
36   --spec init(any()) -> {ok, #httpc_man{}}.
37   -init([{Host, Port, Ssl}, {MaxConnections, ConnectionTimeout}]) ->
38   - State = #httpc_man{
39   - host = Host,
40   - port = Port,
41   - ssl = Ssl,
42   - max_connections = MaxConnections,
43   - connection_timeout = ConnectionTimeout,
44   - sockets = ets:new(sockets, [set])
45   - },
46   - {ok, State}.
  15 +-record(state, {host :: host(),
  16 + port :: port(),
  17 + ssl :: boolean(),
  18 + max_conn :: max_connections(),
  19 + timeout :: timeout(),
  20 + clients :: ets:tid(),
  21 + free=[] :: list()}).
  22 +
  23 +-export_types([host/0, port/0, max_connections/0, connection_timeout/0]).
  24 +-type host() :: inet:ip_address()|string().
  25 +-type port_number() :: 1..65535.
  26 +-type max_connections() :: pos_integer().
  27 +-type connection_timeout() :: timeout().
  28 +
  29 +
  30 +-spec start_link(host(), port_number(), SSL::boolean(),
  31 + max_connections(), connection_timeout()) -> {ok, pid()}.
  32 +start_link(Host, Port, Ssl, MaxConn, ConnTimeout) ->
  33 + gen_server:start_link(?MODULE, {Host, Port, Ssl, MaxConn, ConnTimeout}, []).
  34 +
  35 +-spec checkout(host(), port_number(), SSL::boolean(),
  36 + max_connections(), connection_timeout()) ->
  37 + {ok, port()} | retry_later | no_socket.
  38 +checkout(Host, Port, Ssl, MaxConn, ConnTimeout) ->
  39 + Lb = find_lb({Host,Port,Ssl}, {MaxConn, ConnTimeout}),
  40 + gen_server:call(Lb, {checkout, self()}, infinity).
  41 +
  42 +%% Called when the socket has died and we're done
  43 +-spec checkin(host(), port_number(), SSL::boolean()) -> ok.
  44 +checkin(Host, Port, Ssl) ->
  45 + case find_lb({Host,Port,Ssl}) of
  46 + {error, undefined} -> ok; % LB is dead. Pretend it's fine -- we don't care
  47 + {ok, Pid} -> gen_server:cast(Pid, {checkin, self()})
  48 + end.
  49 +
  50 +%% Called when we're done and the socket can still be reused
  51 +-spec checkin(host(), port_number(), SSL::boolean(), Socket::port()) -> ok.
  52 +checkin(Host, Port, Ssl, Socket) ->
  53 + case find_lb({Host,Port,Ssl}) of
  54 + {error, undefined} ->
  55 + %% should we close the socket? We're not keeping it! There are no
  56 + %% Lbs available!
  57 + ok;
  58 + {ok, Pid} ->
  59 + %% Give ownership back to the server ASAP. The client has to have
  60 + %% kept the socket passive. We rely on its good behaviour.
  61 + %% If the transfer doesn't work, we don't notify.
  62 + case lhttpc_sock:controlling_process(Socket, Pid, Ssl) of
  63 + ok -> gen_server:cast(Pid, {checkin, self(), Socket});
  64 + _ -> ok
  65 + end
  66 + end.
  67 +
  68 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  69 +%%% GEN_SERVER CALLBACKS %%%
  70 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  71 +init({Host,Port,Ssl,MaxConn,ConnTimeout}) ->
  72 + %% we must use insert_new because it is possible a concurrent request is
  73 + %% starting such a server at exactly the same time.
  74 + case ets:insert_new(?MODULE, {{Host,Port,Ssl}, self()}) of
  75 + true ->
  76 + {ok, #state{host=Host,
  77 + port=Port,
  78 + ssl=Ssl,
  79 + max_conn=MaxConn,
  80 + timeout=ConnTimeout,
  81 + clients=ets:new(clients, [set, private])}};
  82 + false ->
  83 + ignore
  84 + end.
  85 +
  86 +handle_call({checkout,Pid}, _From, S = #state{free=[], max_conn=Max, clients=Tid}) ->
  87 + Size = ets:info(Tid, size),
  88 + case Max > Size of
  89 + true ->
  90 + Ref = erlang:monitor(process, Pid),
  91 + ets:insert(Tid, {Pid, Ref}),
  92 + {reply, no_socket, S};
  93 + false ->
  94 + {reply, retry_later, S}
  95 + end;
  96 +handle_call({checkout,Pid}, _From, S = #state{free=[{Taken,Timer}|Free], clients=Tid, ssl=Ssl}) ->
  97 + lhttpc_sock:setopts(Taken, [{active,false}], Ssl),
  98 + case lhttpc_sock:controlling_process(Taken, Pid, Ssl) of
  99 + ok ->
  100 + cancel_timer(Timer, Taken),
  101 + add_client(Tid,Pid),
  102 + {reply, {ok, Taken}, S#state{free=Free}};
  103 + {error, badarg} ->
  104 + %% The caller died.
  105 + lhttpc_sock:setopts(Taken, [{active, once}], Ssl),
  106 + {noreply, S};
  107 + {error, _Reason} -> % socket is closed or something
  108 + cancel_timer(Timer, Taken),
  109 + handle_call({checkout,Pid}, _From, S#state{free=Free})
  110 + end;
  111 +handle_call(_Msg, _From, S) ->
  112 + {noreply, S}.
47 113
48   -%% @hidden
49   --spec handle_call(any(), any(), #httpc_man{}) ->
50   - {reply, any(), #httpc_man{}}.
51   -handle_call({socket, Pid, ConnectOptions, ConnectTimeout}, _, State) ->
52   - {Reply, NewState} = find_socket(Pid, ConnectOptions, ConnectTimeout, State),
53   - {reply, Reply, NewState};
54   -handle_call(_, _, State) ->
55   - {reply, {error, unknown_request}, State}.
56   -
57   -%% @hidden
58   --spec handle_cast(any(), #httpc_man{}) -> {noreply, #httpc_man{}}.
59   -handle_cast({store, Socket}, State) ->
60   - NewState = store_socket(Socket, State),
61   - {noreply, NewState};
62   -handle_cast({remove, Socket}, State) ->
63   - NewState = remove_socket(Socket, State),
64   - {noreply, NewState};
65   -handle_cast({terminate}, State) ->
66   - terminate(undefined, State),
67   - {noreply, State};
68   -handle_cast(_, State) ->
  114 +handle_cast({checkin, Pid}, S = #state{clients=Tid}) ->
  115 + remove_client(Tid, Pid),
  116 + noreply_maybe_shutdown(S);
  117 +handle_cast({checkin, Pid, Socket}, S = #state{ssl=Ssl, clients=Tid, free=Free, timeout=T}) ->
  118 + remove_client(Tid, Pid),
  119 + %% the client cast function took care of giving us ownership
  120 + case lhttpc_sock:setopts(Socket, [{active, once}], Ssl) of
  121 + ok ->
  122 + Timer = start_timer(Socket,T),
  123 + {noreply, S#state{free=[{Socket,Timer}|Free]}};
  124 + {error, _E} -> % socket closed or failed
  125 + noreply_maybe_shutdown(S)
  126 + end;
  127 +handle_cast(_Msg, State) ->
69 128 {noreply, State}.
70 129
71   -%% @hidden
72   --spec handle_info(any(), #httpc_man{}) -> {noreply, #httpc_man{}}.
  130 +handle_info({'DOWN', _Ref, process, Pid, _Reason}, S=#state{clients=Tid}) ->
  131 + %% Client died
  132 + remove_client(Tid,Pid),
  133 + noreply_maybe_shutdown(S);
73 134 handle_info({tcp_closed, Socket}, State) ->
74   - {noreply, remove_socket(Socket, State)};
  135 + noreply_maybe_shutdown(remove_socket(Socket,State));
75 136 handle_info({ssl_closed, Socket}, State) ->
76   - {noreply, remove_socket(Socket, State)};
  137 + noreply_maybe_shutdown(remove_socket(Socket,State));
77 138 handle_info({timeout, Socket}, State) ->
78   - {noreply, remove_socket(Socket, State)};
  139 + noreply_maybe_shutdown(remove_socket(Socket,State));
79 140 handle_info({tcp_error, Socket, _}, State) ->
80   - {noreply, remove_socket(Socket, State)};
  141 + noreply_maybe_shutdown(remove_socket(Socket,State));
81 142 handle_info({ssl_error, Socket, _}, State) ->
82   - {noreply, remove_socket(Socket, State)};
  143 + noreply_maybe_shutdown(remove_socket(Socket,State));
83 144 handle_info({tcp, Socket, _}, State) ->
84   - {noreply, remove_socket(Socket, State)}; % got garbage
  145 + noreply_maybe_shutdown(remove_socket(Socket,State));
85 146 handle_info({ssl, Socket, _}, State) ->
86   - {noreply, remove_socket(Socket, State)}; % got garbage
87   -handle_info(_, State) ->
  147 + noreply_maybe_shutdown(remove_socket(Socket,State));
  148 +handle_info(timeout, State) ->
  149 + {stop, normal, State};
  150 +handle_info(_Info, State) ->
88 151 {noreply, State}.
89 152
90   -%% @hidden
91   --spec terminate(any(), #httpc_man{}) -> ok.
92   -terminate(_, State) ->
93   - close_sockets(State#httpc_man.sockets, State#httpc_man.ssl).
  153 +code_change(_OldVsn, State, _Extra) ->
  154 + {ok, State}.
94 155
95   -%% @hidden
96   --spec code_change(any(), #httpc_man{}, any()) -> #httpc_man{}.
97   -code_change(_, State, _) ->
98   - State.
  156 +terminate(_Reason, #state{host=H, port=P, ssl=Ssl, free=Free, clients=Tid}) ->
  157 + ets:delete(Tid),
  158 + ets:delete(?MODULE,{H,P,Ssl}),
  159 + [lhttpc_sock:close(Socket,Ssl) || Socket <- Free],
  160 + ok.
99 161
100   -find_socket(Pid, ConnectOptions, ConnectTimeout, State = #httpc_man{host=Host, port=Port, ssl=Ssl, sockets=Tid}) ->
101   - case State#httpc_man.available_sockets of
102   - [Socket|Available] ->
103   - case lhttpc_sock:controlling_process(Socket, Pid, Ssl) of
104   - ok ->
105   - [{Socket,Timer}] = ets:lookup(Tid, Socket),
106   - cancel_timer(Timer, Socket),
107   - NewState = State#httpc_man{available_sockets = Available},
108   - {{ok, Socket}, NewState};
109   - {error, badarg} ->
110   - lhttpc_sock:setopts(Socket, [{active, once}], Ssl),
111   - {{error, no_pid}, State};
112   - {error, _Reason} ->
113   - NewState = State#httpc_man{available_sockets = Available},
114   - find_socket(Pid, ConnectOptions, ConnectTimeout, remove_socket(Socket, NewState))
115   - end;
  162 +%%%%%%%%%%%%%%%
  163 +%%% PRIVATE %%%
  164 +%%%%%%%%%%%%%%%
  165 +
  166 +%% Potential race condition: if the lb shuts itself down after a while, it
  167 +%% might happen between a read and the use of the pid. A busy load balancer
  168 +%% should not have this problem.
  169 +-spec find_lb(Name::{host(),port_number(),boolean()}, {max_connections(), connection_timeout()}) -> pid().
  170 +find_lb(Name = {Host,Port,Ssl}, Args={MaxConn, ConnTimeout}) ->
  171 + case ets:lookup(?MODULE, Name) of
116 172 [] ->
117   - MaxSockets = State#httpc_man.max_connections,
118   - Size = ets:info(Tid, size),
119   - Failures = case get('#fail') of
120   - undefined -> 0;
121   - Fail -> Fail
122   - end,
123   - case Failures > MaxSockets of
  173 + case supervisor:start_child(lhttpc_sup, [Host,Port,Ssl,MaxConn,ConnTimeout]) of
  174 + {ok, undefined} -> find_lb(Name,Args);
  175 + {ok, Pid} -> Pid
  176 + end;
  177 + [{_Name, Pid}] ->
  178 + case is_process_alive(Pid) of % lb died, stale entry
  179 + true -> Pid;
124 180 false ->
125   - case MaxSockets > Size andalso Size =/= undefined of
126   - true ->
127   - SocketOptions = [binary, {packet, http}, {active, false} | ConnectOptions],
128   - case lhttpc_sock:connect(Host, Port, SocketOptions, ConnectTimeout, Ssl) of
129   - {ok, Socket} ->
130   - put('#fail', 0),
131   - find_socket(Pid, ConnectOptions, ConnectTimeout, store_socket(Socket, State));
132   - {error, etimedout} ->
133   - {{error, sys_timeout}, State};
134   - {error, timeout} ->
135   - {{error, timeout}, State};
136   - %% client not answering
137   - {error, econnrefused} ->
138   - if Failures < (MaxSockets*2) -> put('#fail', Failures+1);
139   - true -> ok
140   - end,
141   - {{error, econnrefused}, State};
142   - {error, Reason} ->
143   - {{error, Reason}, State}
144   - end;
145   - false ->
146   - {{error, retry_later}, State}
147   - end;
148   - true ->
149   - put('#fail', Failures-2),
150   - {{error, offline}, State}
  181 + ets:delete(?MODULE, Name),
  182 + find_lb(Name,Args)
151 183 end
152 184 end.
153 185
154   -remove_socket(Socket, State = #httpc_man{sockets=Tid, ssl=Ssl}) ->
155   - case ets:lookup(Tid, Socket) of
156   - [{_,Timer}] ->
157   - cancel_timer(Timer, Socket),
158   - lhttpc_sock:close(Socket, Ssl),
159   - ets:delete(Tid, Socket);
160   - [] ->
161   - ok
162   - end,
163   - State.
164   -
165   -store_socket(Socket, State = #httpc_man{connection_timeout=Timeout, ssl=Ssl, sockets=Tid}) ->
166   - Timer = case Timeout of
167   - infinity -> undefined;
168   - _Other -> erlang:send_after(Timeout, self(), {timeout, Socket})
169   - end,
170   - lhttpc_sock:setopts(Socket, [{active, once}], Ssl),
171   - ets:insert(Tid, {Socket, Timer}),
172   - State#httpc_man{available_sockets = [Socket|State#httpc_man.available_sockets]}.
173   -
174   -close_sockets(Sockets, Ssl) ->
175   - ets:foldl(
176   - fun({Socket, undefined}, _) ->
177   - lhttpc_sock:close(Socket, Ssl);
178   - ({Socket, Timer}, _) ->
179   - erlang:cancel_timer(Timer),
180   - lhttpc_sock:close(Socket, Ssl)
181   - end, ok, Sockets
182   - ).
183   -
184   -cancel_timer(undefined, _Socket) ->
185   - ok;
186   -cancel_timer(Timer, Socket) ->
187   - case erlang:cancel_timer(Timer) of
  186 +%% Version of the function to be used when we don't want to start a load balancer
  187 +%% if none is found
  188 +-spec find_lb(Name::{host(),port_number(),boolean()}) -> {error,undefined} | {ok,pid()}.
  189 +find_lb(Name={_Host,_Port,_Ssl}) ->
  190 + case ets:lookup(?MODULE, Name) of
  191 + [] -> {error, undefined};
  192 + [{_Name, Pid}] ->
  193 + case erlang:is_process_alive(Pid) of
  194 + true -> {ok, Pid};
  195 + false -> % lb died, stale entry
  196 + ets:delete(?MODULE,Name),
  197 + {error, undefined}
  198 + end
  199 + end.
  200 +
  201 +-spec add_client(ets:tid(), pid()) -> true.
  202 +add_client(Tid, Pid) ->
  203 + Ref = erlang:monitor(process, Pid),
  204 + ets:insert(Tid, {Pid, Ref}).
  205 +
  206 +-spec remove_client(ets:tid(), pid()) -> true.
  207 +remove_client(Tid, Pid) ->
  208 + case ets:lookup(Tid, Pid) of
  209 + [] -> ok; % client already removed
  210 + [{_Pid, Ref}] ->
  211 + erlang:demonitor(Ref, [flush]),
  212 + ets:delete(Tid, Pid)
  213 + end.
  214 +
  215 +-spec remove_socket(port(), #state{}) -> #state{}.
  216 +remove_socket(Socket, S = #state{ssl=Ssl, free=Free}) ->
  217 + lhttpc_sock:close(Socket, Ssl),
  218 + S#state{free=drop_and_cancel(Socket,Free)}.
  219 +
  220 +-spec drop_and_cancel(port(), [{port(), reference()}]) -> [{port(), reference()}].
  221 +drop_and_cancel(_, []) -> [];
  222 +drop_and_cancel(Socket, [{Socket, TimerRef} | Rest]) ->
  223 + cancel_timer(TimerRef, Socket),
  224 + Rest;
  225 +drop_and_cancel(Socket, [H|T]) ->
  226 + [H | drop_and_cancel(Socket, T)].
  227 +
  228 +-spec cancel_timer(reference(), port()) -> ok.
  229 +cancel_timer(TimerRef, Socket) ->
  230 + case erlang:cancel_timer(TimerRef) of
188 231 false ->
189 232 receive
190 233 {timeout, Socket} -> ok
191   - after
192   - 0 -> ok
  234 + after 0 -> ok
193 235 end;
194 236 _ -> ok
195 237 end.
  238 +
  239 +-spec start_timer(port(), connection_timeout()) -> reference().
  240 +start_timer(_, infinity) -> make_ref(); % dummy timer
  241 +start_timer(Socket, Timeout) ->
  242 + erlang:send_after(Timeout, self(), {timeout,Socket}).
  243 +
  244 +noreply_maybe_shutdown(S=#state{clients=Tid, free=Free}) ->
  245 + case Free =:= [] andalso ets:info(Tid, size) =:= 0 of
  246 + true -> % we're done for
  247 + {noreply,S,?SHUTDOWN_DELAY};
  248 + false ->
  249 + {noreply, S}
  250 + end.
3  src/lhttpc_lib.erl
@@ -28,7 +28,6 @@
28 28 %%% @author Oscar Hellstr�m <oscar@hellstrom.st>
29 29 %%% @doc
30 30 %%% This module implements various library functions used in lhttpc.
31   -%%% @end
32 31 -module(lhttpc_lib).
33 32
34 33 -export([
@@ -177,7 +176,7 @@ format_hdrs(Headers) ->
177 176
178 177 format_hdrs([{Hdr, Value} | T], Acc) ->
179 178 NewAcc = [
180   - maybe_atom_to_list(Hdr), ":", maybe_atom_to_list(Value), "\r\n" | Acc
  179 + maybe_atom_to_list(Hdr), ": ", maybe_atom_to_list(Value), "\r\n" | Acc
181 180 ],
182 181 format_hdrs(T, NewAcc);
183 182 format_hdrs([], Acc) ->
125 src/lhttpc_manager.erl
... ... @@ -1,125 +0,0 @@
1   -%%% ----------------------------------------------------------------------------
2   -%%% Copyright (c) 2009, Erlang Training and Consulting Ltd.
3   -%%% All rights reserved.
4   -%%%
5   -%%% Redistribution and use in source and binary forms, with or without
6   -%%% modification, are permitted provided that the following conditions are met:
7   -%%% * Redistributions of source code must retain the above copyright
8   -%%% notice, this list of conditions and the following disclaimer.
9   -%%% * Redistributions in binary form must reproduce the above copyright
10   -%%% notice, this list of conditions and the following disclaimer in the
11   -%%% documentation and/or other materials provided with the distribution.
12   -%%% * Neither the name of Erlang Training and Consulting Ltd. nor the
13   -%%% names of its contributors may be used to endorse or promote products
14   -%%% derived from this software without specific prior written permission.
15   -%%%
16   -%%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS''
17   -%%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18   -%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19   -%%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE
20   -%%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
21   -%%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22   -%%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23   -%%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24   -%%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25   -%%% ----------------------------------------------------------------------------
26   -
27   -%%% @author Oscar Hellstr�m <oscar@hellstrom.st>
28   -%%% @doc Connection manager for the HTTP client.
29   -%%% This gen_server is responsible for keeping track of persistent
30   -%%% connections to HTTP servers. The only interesting API is
31   -%%% `connection_count/0' and `connection_count/1'.
32   -%%% The gen_server is supposed to be started by a supervisor, which is
33   -%%% normally {@link lhttpc_sup}.
34   -%%% @end
35   --module(lhttpc_manager).
36   -
37   --export([
38   - start_link/0
39   - ]).
40   --export([
41   - init/1,
42   - handle_call/3,
43   - handle_cast/2,
44   - handle_info/2,
45   - code_change/3,
46   - terminate/2
47   - ]).
48   -
49   --behaviour(gen_server).
50   -
51   --record(httpc_man, {
52   - destinations = dict:new()
53   - }).
54   -
55   -%% @spec () -> {ok, pid()}
56   -%% @doc Starts and link to the gen server.
57   -%% This is normally called by a supervisor.
58   -%% @end
59   --spec start_link() -> {ok, pid()} | {error, allready_started}.
60   -start_link() ->
61   - gen_server:start_link({local, ?MODULE}, ?MODULE, nil, []).
62   -
63   -%% @hidden
64   --spec init(any()) -> {ok, #httpc_man{}}.
65   -init(_) ->
66   - process_flag(priority, high),
67   - {ok, #httpc_man{}}.
68   -
69   -%% @hidden
70   --spec handle_call(any(), any(), #httpc_man{}) ->
71   - {reply, any(), #httpc_man{}}.
72   -handle_call({lb, Host, Port, Ssl, MaxConnections, ConnectionTimeout}, _, State) ->
73   - {Reply, NewState} = find_lb({Host, Port, Ssl}, {MaxConnections, ConnectionTimeout}, State),
74   - {reply, Reply, NewState};
75   -handle_call(_, _, State) ->
76   - {reply, {error, unknown_request}, State}.
77   -
78   -%% @hidden
79   --spec handle_cast(any(), #httpc_man{}) -> {noreply, #httpc_man{}}.
80   -handle_cast(_, State) ->
81   - {noreply, State}.
82   -
83   -%% @hidden
84   --spec handle_info(any(), #httpc_man{}) -> {noreply, #httpc_man{}}.
85   -handle_info({kill_client_after_timeout, ReqId, Pid, StreamTo}, State) ->
86   - case erlang:is_process_alive(Pid) of
87   - true ->
88   - exit(Pid, kill),
89   - StreamTo ! {response, ReqId, Pid, {error, timeout}};
90   - false -> ok
91   - end,
92   - {noreply, State};
93   -handle_info(_, State) ->
94   - {noreply, State}.
95   -
96   -%% @hidden
97   --spec terminate(any(), #httpc_man{}) -> ok.
98   -terminate(_, State) ->
99   - close_lbs(State#httpc_man.destinations).
100   -
101   -%% @hidden
102   --spec code_change(any(), #httpc_man{}, any()) -> #httpc_man{}.
103   -code_change(_, State, _) ->
104   - State.
105   -
106   -find_lb(Dest, Options, State) ->
107   - Dests = State#httpc_man.destinations,
108   - case dict:find(Dest, Dests) of
109   - {ok, Lb} ->
110   - {{ok, Lb}, State};
111   - error ->
112   - {ok, Pid} = lhttpc_lb:start_link([Dest, Options]),
113   - NewState = State#httpc_man{
114   - destinations = update_dest(Dest, Pid, Dests)
115   - },
116   - {{ok, Pid}, NewState}
117   - end.
118   -
119   -update_dest(Destination, Lb, Destinations) ->
120   - dict:store(Destination, Lb, Destinations).
121   -
122   -close_lbs(Destinations) ->
123   - lists:foreach(fun({_Dest, Lb}) ->
124   - gen_server:cast(Lb, {terminate})
125   - end, dict:to_list(Destinations)).
3  src/lhttpc_sock.erl
@@ -29,7 +29,6 @@
29 29 %%% @doc
30 30 %%% This module implements wrappers for socket operations.
31 31 %%% Makes it possible to have the same interface to ssl and tcp sockets.
32   -%%% @end
33 32 -module(lhttpc_sock).
34 33
35 34 -export([
@@ -80,7 +79,7 @@ connect(Host, Port, Options, Timeout, false) ->
80 79 recv(Socket, true) ->
81 80 ssl:recv(Socket, 0);
82 81 recv(Socket, false) ->
83   - inet_tcp:recv(Socket, 0).
  82 + gen_tcp:recv(Socket, 0).
84 83
85 84 %% @spec (Socket, Length, SslFlag) -> {ok, Data} | {error, Reason}
86 85 %% Socket = socket()
25 src/lhttpc_sup.erl
@@ -32,7 +32,7 @@
32 32 -module(lhttpc_sup).
33 33 -behaviour(supervisor).
34 34
35   --export([start_link/0]).
  35 +-export([start_link/0, start_link/1]).
36 36 -export([init/1]).
37 37
38 38 -type child() :: {atom(), {atom(), atom(), list(any)},
@@ -46,12 +46,23 @@
46 46 %% @end
47 47 -spec start_link() -> {ok, pid()} | {error, atom()}.
48 48 start_link() ->
49   - supervisor:start_link(?MODULE, nil).
  49 + supervisor:start_link({local, ?MODULE}, ?MODULE, []).
  50 +
  51 +start_link(Args) ->
  52 + supervisor:start_link({local, ?MODULE}, ?MODULE, Args).
50 53
51 54 %% @hidden
52 55 -spec init(any()) -> {ok, {{atom(), integer(), integer()}, [child()]}}.
53   -init(_) ->
54   - LHTTPCManager = {lhttpc_manager, {lhttpc_manager, start_link, []},
55   - permanent, 10000, worker, [lhttpc_manager]
56   - },
57   - {ok, {{one_for_one, 10, 1}, [LHTTPCManager]}}.
  56 +init(Opts) ->
  57 + init_ets(Opts),
  58 + {ok, {{simple_one_for_one, 10, 1}, [
  59 + {load_balancer,
  60 + {lhttpc_lb, start_link, []},
  61 + transient, 10000, worker, [lhttpc_lb]}
  62 + ]}}.
  63 +
  64 +init_ets(Opts) ->
  65 + ETSOpts = proplists:get_value(ets, Opts, []),
  66 + %% Only option supported so far -- others do not really make sense at this point
  67 + ReadConc = {read_concurrency, proplists:get_value(read_concurrency, ETSOpts, false)},
  68 + ets:new(lhttpc_lb, [named_table, set, public, ReadConc]).
29 test/lhttpc_tests.erl
@@ -134,6 +134,7 @@ tcp_test_() ->
134 134 ?_test(bad_url()),
135 135 ?_test(persistent_connection()),
136 136 ?_test(request_timeout()),
  137 + ?_test(connection_timeout()),
137 138 ?_test(suspended_manager()),
138 139 ?_test(chunked_encoding()),
139 140 ?_test(partial_upload_identity()),
@@ -151,7 +152,8 @@ tcp_test_() ->
151 152 ?_test(partial_download_smallish_chunks()),
152 153 ?_test(partial_download_slow_chunks()),
153 154 ?_test(close_connection()),
154   - ?_test(message_queue())
  155 + ?_test(message_queue()),
  156 + ?_test(connection_count()) % just check that it's 0 (last)
155 157 ]}
156 158 }.
157 159
@@ -160,7 +162,8 @@ ssl_test_() ->
160 162 {setup, fun start_app/0, fun stop_app/1, [
161 163 ?_test(ssl_get()),
162 164 ?_test(ssl_post()),
163   - ?_test(ssl_chunked())
  165 + ?_test(ssl_chunked()),
  166 + ?_test(connection_count()) % just check that it's 0 (last)
164 167 ]}
165 168 }.
166 169
@@ -376,6 +379,18 @@ request_timeout() ->
376 379 URL = url(Port, "/slow"),
377 380 ?assertEqual({error, timeout}, lhttpc:request(URL, get, [], 50)).
378 381
  382 +connection_timeout() ->
  383 + Port = start(gen_tcp, [fun simple_response/5, fun simple_response/5]),
  384 + URL = url(Port, "/close_conn"),
  385 + lhttpc_manager:update_connection_timeout(50), % very short keep alive
  386 + {ok, Response} = lhttpc:request(URL, get, [], 100),
  387 + ?assertEqual({200, "OK"}, status(Response)),
  388 + ?assertEqual(<<?DEFAULT_STRING>>, body(Response)),
  389 + timer:sleep(100),
  390 + ?assertEqual(0,
  391 + lhttpc_manager:connection_count({"localhost", Port, false})),
  392 + lhttpc_manager:update_connection_timeout(300000). % set back
  393 +
379 394 suspended_manager() ->
380 395 Port = start(gen_tcp, [fun simple_response/5, fun simple_response/5]),
381 396 URL = url(Port, "/persistent"),
@@ -386,6 +401,8 @@ suspended_manager() ->
386 401 true = erlang:suspend_process(Pid),
387 402 ?assertEqual({error, timeout}, lhttpc:request(URL, get, [], 50)),
388 403 true = erlang:resume_process(Pid),
  404 + ?assertEqual(1,
  405 + lhttpc_manager:connection_count({"localhost", Port, false})),
389 406 {ok, SecondResponse} = lhttpc:request(URL, get, [], 50),
390 407 ?assertEqual({200, "OK"}, status(SecondResponse)),
391 408 ?assertEqual(<<?DEFAULT_STRING>>, body(SecondResponse)).
@@ -467,7 +484,7 @@ partial_upload_chunked() ->
467 484 ?assertEqual(<<?DEFAULT_STRING>>, body(Response1)),
468 485 ?assertEqual("This is chunky stuff!",
469 486 lhttpc_lib:header_value("x-test-orig-body", headers(Response1))),
470   - ?assertEqual(element(2, Trailer),
  487 + ?assertEqual(element(2, Trailer),
471 488 lhttpc_lib:header_value("x-test-orig-trailer-1", headers(Response1))),
472 489 % Make sure it works with no body part in the original request as well
473 490 Headers = [{"Transfer-Encoding", "chunked"}],
@@ -480,7 +497,7 @@ partial_upload_chunked() ->
480 497 ?assertEqual(<<?DEFAULT_STRING>>, body(Response2)),
481 498 ?assertEqual("This is chunky stuff!",
482 499 lhttpc_lib:header_value("x-test-orig-body", headers(Response2))),
483   - ?assertEqual(element(2, Trailer),
  500 + ?assertEqual(element(2, Trailer),
484 501 lhttpc_lib:header_value("x-test-orig-trailer-1", headers(Response2))).
485 502
486 503 partial_upload_chunked_no_trailer() ->
@@ -664,6 +681,10 @@ ssl_chunked() ->
664 681 ?assertEqual("2", lhttpc_lib:header_value("Trailer-2",
665 682 headers(SecondResponse))).
666 683
  684 +connection_count() ->
  685 + timer:sleep(50), % give the TCP stack time to deliver messages
  686 + ?assertEqual(0, lhttpc_manager:connection_count()).
  687 +
667 688 invalid_options() ->
668 689 ?assertError({bad_options, [{foo, bar}, bad_option]},
669 690 lhttpc:request("http://localhost/", get, [], <<>>, 1000,

0 comments on commit 68ab97c

Please sign in to comment.
Something went wrong with that request. Please try again.