blob: 40eb5ddea0af239ec4b5f2a1720b93cfed6d4bef [file] [log] [blame]
David Reiss2c534032008-06-11 00:58:00 +00001%%%-------------------------------------------------------------------
2%%% File : thrift_client.erl
3%%% Author : Todd Lipcon <todd@lipcon.org>
4%%% Description : A client which connects to a thrift service
5%%%
6%%% Created : 24 Feb 2008 by Todd Lipcon <todd@lipcon.org>
7%%%-------------------------------------------------------------------
8-module(thrift_client).
9
10-behaviour(gen_server).
11
12%% API
David Reiss65cf7202008-06-11 01:12:20 +000013-export([start_link/2, start_link/3, start_link/4, call/3, send_call/3, close/1]).
David Reiss2c534032008-06-11 00:58:00 +000014
15%% gen_server callbacks
16-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
17 terminate/2, code_change/3]).
18
19
20-include("thrift_constants.hrl").
21-include("thrift_protocol.hrl").
22
David Reissad74b322008-06-11 01:03:29 +000023-record(state, {service, protocol, seqid}).
David Reiss2c534032008-06-11 00:58:00 +000024
25%%====================================================================
26%% API
27%%====================================================================
28%%--------------------------------------------------------------------
29%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
30%% Description: Starts the server
31%%--------------------------------------------------------------------
David Reissad74b322008-06-11 01:03:29 +000032start_link(Host, Port, Service) when is_integer(Port), is_atom(Service) ->
David Reiss914ebb42008-06-11 01:01:48 +000033 start_link(Host, Port, Service, []).
David Reiss4fd78182008-06-11 01:01:13 +000034
David Reissfc427af2008-06-11 01:11:57 +000035
36%%
37%% Splits client options into protocol options and transport options
38%%
39%% split_options([Options...]) -> {ProtocolOptions, TransportOptions}
40%%
41split_options(Options) ->
42 split_options(Options, [], []).
43
44split_options([], ProtoIn, TransIn) ->
45 {ProtoIn, TransIn};
46
47split_options([Opt = {OptKey, _} | Rest], ProtoIn, TransIn)
48 when OptKey =:= strict_read;
49 OptKey =:= strict_write ->
50 split_options(Rest, [Opt | ProtoIn], TransIn);
51
52split_options([Opt = {OptKey, _} | Rest], ProtoIn, TransIn)
53 when OptKey =:= framed;
54 OptKey =:= connect_timeout;
55 OptKey =:= sockopts ->
56 split_options(Rest, ProtoIn, [Opt | TransIn]).
57
58
59%% Backwards-compatible starter for the common-case of socket transports
David Reiss2fe905e2008-06-11 01:02:23 +000060start_link(Host, Port, Service, Options)
61 when is_integer(Port), is_atom(Service), is_list(Options) ->
David Reissfc427af2008-06-11 01:11:57 +000062 {ProtoOpts, TransOpts} = split_options(Options),
63
64 {ok, TransportFactory} =
65 thrift_socket_transport:new_transport_factory(Host, Port, TransOpts),
66
67 {ok, ProtocolFactory} = thrift_binary_protocol:new_protocol_factory(
68 TransportFactory, ProtoOpts),
69
David Reiss44f785e2008-06-11 01:03:37 +000070 start_link(ProtocolFactory, Service).
David Reissad74b322008-06-11 01:03:29 +000071
David Reissfc427af2008-06-11 01:11:57 +000072
David Reiss44f785e2008-06-11 01:03:37 +000073%% ProtocolFactory :: fun() -> thrift_protocol()
74start_link(ProtocolFactory, Service)
75 when is_function(ProtocolFactory), is_atom(Service) ->
David Reissad74b322008-06-11 01:03:29 +000076 case gen_server:start_link(?MODULE, [Service], []) of
David Reisse5a4d0c2008-06-11 01:02:10 +000077 {ok, Pid} ->
David Reiss44f785e2008-06-11 01:03:37 +000078 case gen_server:call(Pid, {connect, ProtocolFactory}) of
David Reisse5a4d0c2008-06-11 01:02:10 +000079 ok ->
80 {ok, Pid};
81 Error ->
82 Error
83 end;
84 Else ->
85 Else
86 end.
David Reiss2c534032008-06-11 00:58:00 +000087
David Reiss2c534032008-06-11 00:58:00 +000088call(Client, Function, Args)
89 when is_pid(Client), is_atom(Function), is_list(Args) ->
90 case gen_server:call(Client, {call, Function, Args}) of
91 R = {ok, _} -> R;
92 R = {error, _} -> R;
93 {exception, Exception} -> throw(Exception)
94 end.
David Reiss2c534032008-06-11 00:58:00 +000095
David Reissa2f45972008-06-11 01:13:33 +000096cast(Client, Function, Args)
97 when is_pid(Client), is_atom(Function), is_list(Args) ->
98 gen_server:cast(Client, {call, Function, Args}).
99
David Reiss65cf7202008-06-11 01:12:20 +0000100%% Sends a function call but does not read the result. This is useful
David Reissc51986f2009-03-24 20:01:25 +0000101%% if you're trying to log non-oneway function calls to write-only
David Reiss65cf7202008-06-11 01:12:20 +0000102%% transports like thrift_disk_log_transport.
103send_call(Client, Function, Args)
104 when is_pid(Client), is_atom(Function), is_list(Args) ->
105 gen_server:call(Client, {send_call, Function, Args}).
106
David Reiss464e3002008-06-11 01:00:45 +0000107close(Client) when is_pid(Client) ->
David Reiss6f1cd532008-06-11 01:01:21 +0000108 gen_server:cast(Client, close).
David Reiss464e3002008-06-11 01:00:45 +0000109
David Reiss2c534032008-06-11 00:58:00 +0000110%%====================================================================
111%% gen_server callbacks
112%%====================================================================
113
114%%--------------------------------------------------------------------
115%% Function: init(Args) -> {ok, State} |
116%% {ok, State, Timeout} |
117%% ignore |
118%% {stop, Reason}
119%% Description: Initiates the server
120%%--------------------------------------------------------------------
David Reissad74b322008-06-11 01:03:29 +0000121init([Service]) ->
122 {ok, #state{service = Service}}.
David Reiss2c534032008-06-11 00:58:00 +0000123
124%%--------------------------------------------------------------------
125%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
126%% {reply, Reply, State, Timeout} |
127%% {noreply, State} |
128%% {noreply, State, Timeout} |
129%% {stop, Reason, Reply, State} |
130%% {stop, Reason, State}
131%% Description: Handling call messages
132%%--------------------------------------------------------------------
David Reiss44f785e2008-06-11 01:03:37 +0000133handle_call({connect, ProtocolFactory}, _From,
David Reissad74b322008-06-11 01:03:29 +0000134 State = #state{service = Service}) ->
David Reiss44f785e2008-06-11 01:03:37 +0000135 case ProtocolFactory() of
David Reissad74b322008-06-11 01:03:29 +0000136 {ok, Protocol} ->
137 {reply, ok, State#state{protocol = Protocol,
138 seqid = 0}};
David Reisse5a4d0c2008-06-11 01:02:10 +0000139 Error ->
140 {stop, normal, Error, State}
141 end;
142
David Reiss65cf7202008-06-11 01:12:20 +0000143handle_call({call, Function, Args}, _From, State = #state{service = Service}) ->
144 Result = catch_function_exceptions(
145 fun() ->
146 ok = send_function_call(State, Function, Args),
147 receive_function_result(State, Function)
148 end,
149 Service),
150 {reply, Result, State};
David Reiss6b3e40f2008-06-11 00:59:03 +0000151
David Reiss65cf7202008-06-11 01:12:20 +0000152
153handle_call({send_call, Function, Args}, _From, State = #state{service = Service}) ->
154 Result = catch_function_exceptions(
155 fun() ->
156 send_function_call(State, Function, Args)
157 end,
158 Service),
David Reiss6f1cd532008-06-11 01:01:21 +0000159 {reply, Result, State}.
David Reiss2c534032008-06-11 00:58:00 +0000160
David Reiss65cf7202008-06-11 01:12:20 +0000161
162%% Helper function that catches exceptions thrown by sending or receiving
163%% a function and returns the correct response for call or send_only above.
164catch_function_exceptions(Fun, Service) ->
165 try
166 Fun()
167 catch
168 throw:{return, Return} ->
169 Return;
170 error:function_clause ->
171 ST = erlang:get_stacktrace(),
172 case hd(ST) of
173 {Service, function_info, [Function, _]} ->
174 {error, {no_function, Function}};
175 _ -> throw({error, {function_clause, ST}})
176 end
177 end.
178
179
David Reiss2c534032008-06-11 00:58:00 +0000180%%--------------------------------------------------------------------
181%% Function: handle_cast(Msg, State) -> {noreply, State} |
182%% {noreply, State, Timeout} |
183%% {stop, Reason, State}
184%% Description: Handling cast messages
185%%--------------------------------------------------------------------
David Reissa2f45972008-06-11 01:13:33 +0000186handle_cast({call, Function, Args}, State = #state{service = Service,
187 protocol = Protocol,
188 seqid = SeqId}) ->
189 _Result =
190 try
191 ok = send_function_call(State, Function, Args),
192 receive_function_result(State, Function)
193 catch
194 Class:Reason ->
195 error_logger:error_msg("error ignored in handle_cast({cast,...},...): ~p:~p~n", [Class, Reason])
196 end,
197
198 {noreply, State};
199
David Reiss6f1cd532008-06-11 01:01:21 +0000200handle_cast(close, State=#state{protocol = Protocol}) ->
201%% error_logger:info_msg("thrift_client ~p received close", [self()]),
202 {stop,normal,State};
David Reiss2c534032008-06-11 00:58:00 +0000203handle_cast(_Msg, State) ->
204 {noreply, State}.
205
206%%--------------------------------------------------------------------
207%% Function: handle_info(Info, State) -> {noreply, State} |
208%% {noreply, State, Timeout} |
209%% {stop, Reason, State}
210%% Description: Handling all non call/cast messages
211%%--------------------------------------------------------------------
212handle_info(_Info, State) ->
213 {noreply, State}.
214
215%%--------------------------------------------------------------------
216%% Function: terminate(Reason, State) -> void()
217%% Description: This function is called by a gen_server when it is about to
218%% terminate. It should be the opposite of Module:init/1 and do any necessary
219%% cleaning up. When it returns, the gen_server terminates with Reason.
220%% The return value is ignored.
221%%--------------------------------------------------------------------
David Reisse5a4d0c2008-06-11 01:02:10 +0000222terminate(Reason, State = #state{protocol=undefined}) ->
223 ok;
224terminate(Reason, State = #state{protocol=Protocol}) ->
David Reiss464e3002008-06-11 01:00:45 +0000225 thrift_protocol:close_transport(Protocol),
David Reiss2c534032008-06-11 00:58:00 +0000226 ok.
227
228%%--------------------------------------------------------------------
229%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
230%% Description: Convert process state when code is changed
231%%--------------------------------------------------------------------
232code_change(_OldVsn, State, _Extra) ->
233 {ok, State}.
234
235%%--------------------------------------------------------------------
236%%% Internal functions
237%%--------------------------------------------------------------------
238send_function_call(#state{protocol = Proto,
239 service = Service,
240 seqid = SeqId},
241 Function,
242 Args) ->
243 Params = Service:function_info(Function, params_type),
244 {struct, PList} = Params,
David Reissc5257452008-06-11 00:59:27 +0000245 if
246 length(PList) =/= length(Args) ->
David Reiss2c534032008-06-11 00:58:00 +0000247 throw({return, {error, {bad_args, Function, Args}}});
David Reissc5257452008-06-11 00:59:27 +0000248 true -> ok
David Reiss2c534032008-06-11 00:58:00 +0000249 end,
250
251 Begin = #protocol_message_begin{name = atom_to_list(Function),
252 type = ?tMessageType_CALL,
253 seqid = SeqId},
254 ok = thrift_protocol:write(Proto, Begin),
255 ok = thrift_protocol:write(Proto, {Params, list_to_tuple([Function | Args])}),
256 ok = thrift_protocol:write(Proto, message_end),
257 thrift_protocol:flush_transport(Proto),
258 ok.
259
David Reiss2c534032008-06-11 00:58:00 +0000260receive_function_result(State = #state{protocol = Proto,
261 service = Service},
262 Function) ->
263 ResultType = Service:function_info(Function, reply_type),
264 read_result(State, Function, ResultType).
265
266read_result(_State,
267 _Function,
268 async_void) ->
269 {ok, ok};
270
271read_result(State = #state{protocol = Proto,
272 seqid = SeqId},
273 Function,
274 ReplyType) ->
275 case thrift_protocol:read(Proto, message_begin) of
276 #protocol_message_begin{seqid = RetSeqId} when RetSeqId =/= SeqId ->
277 {error, {bad_seq_id, SeqId}};
David Reiss6b3e40f2008-06-11 00:59:03 +0000278
David Reiss2c534032008-06-11 00:58:00 +0000279 #protocol_message_begin{type = ?tMessageType_EXCEPTION} ->
280 handle_application_exception(State);
David Reiss6b3e40f2008-06-11 00:59:03 +0000281
David Reiss2c534032008-06-11 00:58:00 +0000282 #protocol_message_begin{type = ?tMessageType_REPLY} ->
283 handle_reply(State, Function, ReplyType)
284 end.
285
David Reiss2c534032008-06-11 00:58:00 +0000286handle_reply(State = #state{protocol = Proto,
287 service = Service},
288 Function,
289 ReplyType) ->
290 {struct, ExceptionFields} = Service:function_info(Function, exceptions),
291 ReplyStructDef = {struct, [{0, ReplyType}] ++ ExceptionFields},
David Reiss2c534032008-06-11 00:58:00 +0000292 {ok, Reply} = thrift_protocol:read(Proto, ReplyStructDef),
293 ReplyList = tuple_to_list(Reply),
294 true = length(ReplyList) == length(ExceptionFields) + 1,
295 ExceptionVals = tl(ReplyList),
296 Thrown = [X || X <- ExceptionVals,
297 X =/= undefined],
David Reiss2c534032008-06-11 00:58:00 +0000298 Result =
299 case Thrown of
300 [] when ReplyType == {struct, []} ->
301 {ok, ok};
302 [] ->
303 {ok, hd(ReplyList)};
304 [Exception] ->
305 {exception, Exception}
306 end,
307 ok = thrift_protocol:read(Proto, message_end),
308 Result.
David Reiss6b3e40f2008-06-11 00:59:03 +0000309
David Reiss55ff70f2008-06-11 00:58:25 +0000310handle_application_exception(State = #state{protocol = Proto}) ->
311 {ok, Exception} = thrift_protocol:read(Proto,
312 ?TApplicationException_Structure),
313 ok = thrift_protocol:read(Proto, message_end),
314 XRecord = list_to_tuple(
315 ['TApplicationException' | tuple_to_list(Exception)]),
David Reiss1af18682008-06-11 01:01:36 +0000316 error_logger:error_msg("X: ~p~n", [XRecord]),
David Reiss55ff70f2008-06-11 00:58:25 +0000317 true = is_record(XRecord, 'TApplicationException'),
318 {exception, XRecord}.