Use new performant thrift codec infrastructure (#145)

* Implement woody_client_thrift_v2 over new thrift codec facility, make it default
* Move tracing facilities into woody_trace_h
* Implement woody_server_thrift_v2 over thrift codec facility, make it default
* Bump to rbkmoney/genlib@54920e76
* Enforce tuple-based args representation
* Enable cross tests between impls
* Switch to master rbkmoney/thrift_erlang@4eda678c
This commit is contained in:
Andrew Mayorov 2020-08-18 13:01:47 +03:00 committed by GitHub
parent 42dbb91b2b
commit d106ef66bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1481 additions and 296 deletions

View File

@ -48,7 +48,7 @@ Erlang реализация [Библиотеки RPC вызовов для об
```erlang
7> Url = <<"localhost:8022/v1/thrift_money_service">>.
8> Function = give_me_money. %% thrift метод
9> Args = [100, <<"rub">>].
9> Args = {100, <<"rub">>}.
10> Request = {Service, Function, Args}.
11> ClientEventHandler = {my_event_handler, MyCustomOptions}.
12> Context1 = woody_context:new(<<"myUniqRequestID1">>).
@ -119,7 +119,7 @@ Erlang реализация [Библиотеки RPC вызовов для об
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) ->
{ok, woody:result()} | no_return().
handle_function(give_me_money, Sum = [Amount, Currency], Context, _MyOpts) ->
handle_function(give_me_money, Sum = {Amount, Currency}, Context, _MyOpts) ->
%% RpcId можно получить из Context, полученного handle_function,
%% для использования при логировании.

View File

@ -8,12 +8,17 @@
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_style, no_tabs},
{elvis_style, no_trailing_whitespace},
{elvis_style, macro_module_names},
{elvis_style, operator_spaces, #{rules => [{right, ","}, {right, "++"}, {left, "++"}]}},
{elvis_style, nesting_level, #{level => 3}},
{elvis_style, god_modules, #{limit => 25, ignore => [woody_tests_SUITE]}},
{elvis_style, no_if_expression},
{elvis_style, invalid_dynamic_call, #{ignore => [woody_client_thrift, woody_event_formatter]}},
{elvis_style, invalid_dynamic_call, #{
ignore => [
woody_client_thrift,
woody_event_formatter,
woody_util
]
}},
{elvis_style, used_ignored_variable},
{elvis_style, no_behavior_info},
{elvis_style, module_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*(_SUITE)?$"}},

View File

@ -14,7 +14,7 @@
1},
{<<"genlib">>,
{git,"https://github.com/rbkmoney/genlib.git",
{ref,"a6b9f52d61372cdbea6c5967053662dd3309e0d7"}},
{ref,"54920e768a71f121304a5eda547ee60295398f3c"}},
0},
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.8.0">>},0},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.2">>},0},
@ -34,7 +34,7 @@
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.5">>},1},
{<<"thrift">>,
{git,"https://github.com/rbkmoney/thrift_erlang.git",
{ref,"aa233e29a8d8682ae9e088eedde772f0ee45d105"}},
{ref,"4eda678c985d2894251b91ae43aacf7941846cc9"}},
0},
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},2}]}.
[

View File

@ -34,7 +34,7 @@
-type service_name() :: atom().
-type service() :: {module(), service_name()}.
-type func() :: atom().
-type args() :: list().
-type args() :: tuple().
-type request() :: {service(), func(), args()}.
-type result() :: _.
-type th_handler() :: {service(), handler(options())}.

View File

@ -224,6 +224,6 @@ add_thrift_meta({Service = {_, ServiceName}, Function, Args}, Meta) ->
service => ServiceName,
service_schema => Service,
function => Function,
type => woody_client_thrift:get_rpc_type(Service, Function),
type => woody_util:get_rpc_type(Service, Function),
args => Args
}.

View File

@ -10,16 +10,17 @@
-export([call /2]).
-export([call /3]).
-type transport_options() :: woody_client_thrift_http_transport:transport_options().
%% Types
-type options() :: #{
url := woody:url(),
event_handler := woody:ev_handlers(),
transport_opts => transport_options(), %% See hackney:request/5 for available options.
resolver_opts => woody_resolver:options(),
protocol => thrift,
transport => http
transport => http,
%% Set to override protocol handler module selection, useful for test purposes, rarely
%% if ever needed otherwise.
protocol_handler_override => module(),
%% Implementation-specific options
_ => _
}.
-export_type([options/0]).

View File

@ -9,9 +9,16 @@
-export([call /3]).
-export([child_spec/1]).
-export([get_rpc_type/2]).
%% Types
-type options() :: #{
url := woody:url(),
event_handler := woody:ev_handlers(),
transport_opts => woody_client_thrift_http_transport:transport_options(),
resolver_opts => woody_resolver:options(),
protocol => thrift,
transport => http
}.
-type thrift_client() :: term().
-define(WOODY_OPTS, [protocol, transport, event_handler]).
@ -19,12 +26,12 @@
%%
%% API
%%
-spec child_spec(woody_client:options()) ->
-spec child_spec(options()) ->
supervisor:child_spec().
child_spec(Options) ->
woody_client_thrift_http_transport:child_spec(get_transport_opts(Options)).
-spec call(woody:request(), woody_client:options(), woody_state:st()) ->
-spec call(woody:request(), options(), woody_state:st()) ->
woody_client:result().
call({Service = {_, ServiceName}, Function, Args}, Opts, WoodyState) ->
WoodyContext = woody_state:get_context(WoodyState),
@ -33,7 +40,7 @@ call({Service = {_, ServiceName}, Function, Args}, Opts, WoodyState) ->
service => ServiceName,
service_schema => Service,
function => Function,
type => get_rpc_type(Service, Function),
type => woody_util:get_rpc_type(Service, Function),
args => Args,
deadline => woody_context:get_deadline(WoodyContext),
metadata => woody_context:get_meta(WoodyContext)
@ -43,19 +50,10 @@ call({Service = {_, ServiceName}, Function, Args}, Opts, WoodyState) ->
_ = log_event(?EV_CALL_SERVICE, WoodyState1, #{}),
do_call(make_thrift_client(Service, Opts, WoodyState1), Function, Args, WoodyState1).
-spec get_rpc_type(woody:service(), woody:func()) ->
woody:rpc_type().
get_rpc_type(ThriftService = {Module, Service}, Function) ->
try woody_util:get_rpc_reply_type(Module:function_info(Service, Function, reply_type))
catch
error:Reason when Reason =:= undef orelse Reason =:= badarg ->
error(badarg, [ThriftService, Function])
end.
%%
%% Internal functions
%%
-spec make_thrift_client(woody:service(), woody_client:options(), woody_state:st()) ->
-spec make_thrift_client(woody:service(), options(), woody_state:st()) ->
thrift_client().
make_thrift_client(Service, Opts = #{url := Url}, WoodyState) ->
{ok, Protocol} = thrift_binary_protocol:new(
@ -70,12 +68,12 @@ make_thrift_client(Service, Opts = #{url := Url}, WoodyState) ->
{ok, Client} = thrift_client:new(Protocol, Service),
Client.
-spec get_transport_opts(woody_client:options()) ->
-spec get_transport_opts(options()) ->
woody_client_thrift_http_transport:transport_options().
get_transport_opts(Opts) ->
maps:get(transport_opts, Opts, #{}).
-spec get_resolver_opts(woody_client:options()) ->
-spec get_resolver_opts(options()) ->
woody_resolver:options().
get_resolver_opts(Opts) ->
maps:get(resolver_opts, Opts, #{}).
@ -83,7 +81,7 @@ get_resolver_opts(Opts) ->
-spec do_call(thrift_client(), woody:func(), woody:args(), woody_state:st()) ->
woody_client:result().
do_call(Client, Function, Args, WoodyState) ->
{ClientNext, Result} = try thrift_client:call(Client, Function, Args)
{ClientNext, Result} = try thrift_client:call(Client, Function, tuple_to_list(Args))
catch
throw:{Client1, {exception, #'TApplicationException'{}}} ->
{Client1, {error, {system, get_server_violation_error()}}};

View File

@ -15,6 +15,8 @@
%% Types
%% See hackney:request/5 for available options.
-type transport_options() :: map().
-export_type([transport_options/0]).

View File

@ -0,0 +1,392 @@
-module(woody_client_thrift_v2).
-behaviour(woody_client_behaviour).
-include_lib("thrift/include/thrift_constants.hrl").
-include("woody_defs.hrl").
%% woody_client_behaviour callback
-export([call /3]).
-export([child_spec/1]).
%% Types
-type options() :: #{
url := woody:url(),
event_handler := woody:ev_handlers(),
transport_opts => transport_options(),
resolver_opts => woody_resolver:options(),
protocol => thrift,
transport => http
}.
%% See hackney:request/5 for available options.
-type transport_options() :: map().
-define(DEFAULT_CONNECT_AND_SEND_TIMEOUT, 1000). %% millisec
-define(DEFAULT_TRANSPORT_OPTIONS, #{
connect_options => [
% Turn TCP_NODELAY on.
% We expect that Nagle's algorithm would not be very helpful for typical
% Woody RPC workloads, negatively impacting perceived latency. So it's
% better to turn it off.
{nodelay, true}
]
}).
%%
%% API
%%
-spec child_spec(options()) ->
supervisor:child_spec().
child_spec(Options) ->
TransportOpts = get_transport_opts(Options),
Name = maps:get(pool, TransportOpts, undefined),
hackney_pool:child_spec(Name, maps:to_list(TransportOpts)).
-spec call(woody:request(), options(), woody_state:st()) ->
woody_client:result().
call({Service = {_, ServiceName}, Function, Args}, Opts, WoodyState) ->
WoodyContext = woody_state:get_context(WoodyState),
WoodyState1 = woody_state:add_ev_meta(
#{
service => ServiceName,
service_schema => Service,
function => Function,
type => woody_util:get_rpc_type(Service, Function),
args => Args,
deadline => woody_context:get_deadline(WoodyContext),
metadata => woody_context:get_meta(WoodyContext)
},
WoodyState
),
_ = log_event(?EV_CALL_SERVICE, WoodyState1, #{}),
do_call(Service, Function, Args, Opts, WoodyState1).
%%
%% Internal functions
%%
-include_lib("hackney/include/hackney_lib.hrl").
-type http_headers() :: [{binary(), binary()}].
-type header_parse_value() :: none | woody:http_header_val().
-define(CODEC, thrift_strict_binary_codec).
-define(ERROR_RESP_BODY , <<"parse http response body error">> ).
-define(ERROR_RESP_HEADER , <<"parse http response headers error">>).
-define(BAD_RESP_HEADER , <<"reason unknown due to bad ", ?HEADER_PREFIX/binary, "-error- headers">>).
-define(SERVER_VIOLATION_ERROR,
{external, result_unexpected, <<
"server violated thrift protocol: "
"sent TApplicationException (unknown exception) with http code 200"
>>}
).
-spec get_transport_opts(options()) ->
woody_client_thrift_http_transport:transport_options().
get_transport_opts(Opts) ->
maps:get(transport_opts, Opts, #{}).
-spec get_resolver_opts(options()) ->
woody_resolver:options().
get_resolver_opts(Opts) ->
maps:get(resolver_opts, Opts, #{}).
-spec do_call(woody:service(), woody:func(), woody:args(), options(), woody_state:st()) ->
woody_client:result().
do_call(Service, Function, Args, Opts, WoodyState) ->
Buffer = ?CODEC:new(),
Result = case thrift_client_codec:write_function_call(Buffer, ?CODEC, Service, Function, Args, 0) of
{ok, Buffer1} ->
case send_call(Buffer1, Opts, WoodyState) of
{ok, Response} ->
handle_result(Service, Function, Response);
Error ->
Error
end;
Error ->
Error
end,
log_result(Result, WoodyState),
map_result(Result).
-spec handle_result(woody:service(), woody:func(), binary()) ->
_Result.
handle_result(Service, Function, Response) ->
Buffer = ?CODEC:new(Response),
case thrift_client_codec:read_function_result(Buffer, ?CODEC, Service, Function, 0) of
{ok, Result, Leftovers} ->
Bytes = ?CODEC:close(Leftovers),
case Result of
ok when Bytes == <<>> ->
{ok, ok};
{reply, Reply} when Bytes == <<>> ->
{ok, Reply};
{exception, #'TApplicationException'{}} when Bytes == <<>> ->
{error, {system, ?SERVER_VIOLATION_ERROR}};
{exception, Exception} when Bytes == <<>> ->
{error, {business, Exception}};
_ ->
{error, {excess_response_body, Bytes, Result}}
end;
{error, _} = Error ->
Error
end.
-spec send_call(?CODEC:buffer(), options(), woody_state:st()) ->
{ok, binary()} | {error, {system, _}}.
send_call(Buffer, Opts = #{url := Url}, WoodyState) ->
Context = woody_state:get_context(WoodyState),
TransportOpts = get_transport_opts(Opts),
ResolverOpts = get_resolver_opts(Opts),
case is_deadline_reached(Context) of
true ->
_ = log_event(?EV_INTERNAL_ERROR, WoodyState, #{status => error, reason => <<"Deadline reached">>}),
{error, {system, {internal, resource_unavailable, <<"deadline reached">>}}};
false ->
_ = log_event(?EV_CLIENT_SEND, WoodyState, #{url => Url}),
% MSPF-416: We resolve url host to an ip here to prevent
% reusing keep-alive connections to dead hosts
case woody_resolver:resolve_url(Url, WoodyState, ResolverOpts) of
{ok, {OldUrl, NewUrl}} ->
Headers = add_host_header(OldUrl, make_woody_headers(Context)),
TransportOpts1 = set_defaults(TransportOpts),
TransportOpts2 = set_timeouts(TransportOpts1, Context),
Result = hackney:request(post, NewUrl, Headers, Buffer, maps:to_list(TransportOpts2)),
handle_response(Result, WoodyState);
{error, Reason} ->
Error = {error, {resolve_failed, Reason}},
handle_response(Error, WoodyState)
end
end.
is_deadline_reached(Context) ->
woody_deadline:is_reached(woody_context:get_deadline(Context)).
set_defaults(Options) ->
maps:merge(?DEFAULT_TRANSPORT_OPTIONS, Options).
set_timeouts(Options, Context) ->
case woody_context:get_deadline(Context) of
undefined ->
Options;
Deadline ->
Timeout = woody_deadline:to_unixtime_ms(Deadline) - woody_deadline:unow(),
ConnectTimeout = SendTimeout = calc_timeouts(Timeout),
%% It is intentional, that application can override the timeout values
%% calculated from the deadline (first option value in the list takes
%% the precedence).
maps:merge(#{
connect_timeout => ConnectTimeout,
send_timeout => SendTimeout,
recv_timeout => Timeout
}, Options)
end.
calc_timeouts(Timeout) ->
%% It is assumed that connect and send timeouts each
%% should take no more than 20% of the total request time
%% and in any case no more, than DEFAULT_CONNECT_AND_SEND_TIMEOUT together.
case max(0, Timeout) div 5 of
T when (T*2) > ?DEFAULT_CONNECT_AND_SEND_TIMEOUT ->
?DEFAULT_CONNECT_AND_SEND_TIMEOUT;
T ->
T
end.
-spec make_woody_headers(woody_context:ctx()) ->
http_headers().
make_woody_headers(Context) ->
add_optional_headers(Context, [
{<<"content-type">> , ?CONTENT_TYPE_THRIFT},
{<<"accept">> , ?CONTENT_TYPE_THRIFT},
{?HEADER_RPC_ROOT_ID , woody_context:get_rpc_id(trace_id , Context)},
{?HEADER_RPC_ID , woody_context:get_rpc_id(span_id , Context)},
{?HEADER_RPC_PARENT_ID , woody_context:get_rpc_id(parent_id, Context)}
]).
-spec add_optional_headers(woody_context:ctx(), http_headers()) ->
http_headers().
add_optional_headers(Context, Headers) ->
add_deadline_header(Context, add_metadata_headers(Context, Headers)).
-spec add_metadata_headers(woody_context:ctx(), http_headers()) ->
http_headers().
add_metadata_headers(Context, Headers) ->
maps:fold(fun add_metadata_header/3, Headers, woody_context:get_meta(Context)).
-spec add_metadata_header(woody:http_header_name(), woody:http_header_val(), http_headers()) ->
http_headers() | no_return().
add_metadata_header(H, V, Headers) when is_binary(H) and is_binary(V) ->
[{<<?HEADER_META_PREFIX/binary, H/binary>>, V} | Headers].
add_deadline_header(Context, Headers) ->
do_add_deadline_header(woody_context:get_deadline(Context), Headers).
do_add_deadline_header(undefined, Headers) ->
Headers;
do_add_deadline_header(Deadline, Headers) ->
[{?HEADER_DEADLINE, woody_deadline:to_binary(Deadline)} | Headers].
add_host_header(#hackney_url{netloc = Netloc}, Headers) ->
[{<<"Host">>, Netloc} | Headers].
-spec handle_response(_, woody_state:st()) ->
{ok, woody:http_body()} | {error, {system, woody_error:system_error()}}.
handle_response({ok, 200, Headers, Ref}, WoodyState) ->
Meta = case check_error_reason(Headers, 200, WoodyState) of
<<>> -> #{};
Reason -> #{reason => Reason}
end,
_ = log_event(?EV_CLIENT_RECEIVE, WoodyState, Meta#{status => ok, code => 200}),
get_body(hackney:body(Ref), WoodyState);
handle_response({ok, Code, Headers, Ref}, WoodyState) ->
{Class, Details} = check_error_headers(Code, Headers, WoodyState),
_ = log_event(?EV_CLIENT_RECEIVE, WoodyState, #{status => error, code => Code, reason => Details}),
%% Free the connection
case hackney:skip_body(Ref) of
ok ->
ok;
{error, Reason} ->
_ = log_event(?EV_INTERNAL_ERROR, WoodyState, #{status => error, reason => woody_util:to_binary(Reason)})
end,
{error, {system, {external, Class, Details}}};
handle_response({error, {closed, _}}, WoodyState) ->
Reason = <<"partial response">>,
_ = log_event(?EV_CLIENT_RECEIVE, WoodyState, #{status => error, reason => Reason}),
{error, {system, {external, result_unknown, Reason}}};
handle_response({error, Reason}, WoodyState) when
Reason =:= timeout ;
Reason =:= econnaborted ;
Reason =:= enetreset ;
Reason =:= econnreset ;
Reason =:= eshutdown ;
Reason =:= etimedout ;
Reason =:= closed
->
BinReason = woody_util:to_binary(Reason),
_ = log_event(?EV_CLIENT_RECEIVE, WoodyState, #{status => error, reason => BinReason}),
{error, {system, {external, result_unknown, BinReason}}};
handle_response({error, Reason}, WoodyState) when
Reason =:= econnrefused ;
Reason =:= connect_timeout ;
Reason =:= checkout_timeout;
Reason =:= enetdown ;
Reason =:= enetunreach ;
Reason =:= ehostunreach ;
Reason =:= eacces ;
element(1, Reason) =:= resolve_failed
->
BinReason = woody_error:format_details(Reason),
_ = log_event(?EV_CLIENT_RECEIVE, WoodyState, #{status => error, reason => BinReason}),
{error, {system, {internal, resource_unavailable, BinReason}}};
handle_response(Error = {error, {system, _}}, _) ->
Error;
handle_response({error, Reason}, WoodyState) ->
Details = woody_error:format_details(Reason),
_ = log_event(?EV_CLIENT_RECEIVE, WoodyState, #{status => error, reason => Details}),
{error, {system, {internal, result_unexpected, Details}}}.
-spec check_error_reason(http_headers(), woody:http_code(), woody_state:st()) ->
woody_error:details().
check_error_reason(Headers, Code, WoodyState) ->
do_check_error_reason(get_header_value(?HEADER_E_REASON, Headers), Code, WoodyState).
-spec do_check_error_reason(header_parse_value(), woody:http_code(), woody_state:st()) ->
woody_error:details().
do_check_error_reason(none, 200, _WoodyState) ->
<<>>;
do_check_error_reason(none, Code, WoodyState) ->
_ = log_event(?EV_TRACE, WoodyState, #{event => woody_util:to_binary([?HEADER_E_REASON, " header missing"])}),
woody_util:to_binary(["got response with http code ", Code, " and without ", ?HEADER_E_REASON, " header"]);
do_check_error_reason(Reason, _, _) ->
Reason.
-spec check_error_headers(woody:http_code(), http_headers(), woody_state:st()) ->
{woody_error:class(), woody_error:details()}.
check_error_headers(502, Headers, WoodyState) ->
check_502_error_class(get_error_class_header_value(Headers), Headers, WoodyState);
check_error_headers(Code, Headers, WoodyState) ->
{get_error_class(Code), check_error_reason(Headers, Code, WoodyState)}.
-spec get_error_class(woody:http_code()) ->
woody_error:class().
get_error_class(503) ->
resource_unavailable;
get_error_class(504) ->
result_unknown;
get_error_class(_) ->
result_unexpected.
-spec check_502_error_class(header_parse_value(), http_headers(), woody_state:st()) ->
{woody_error:class(), woody_error:details()}.
check_502_error_class(none, Headers, WoodyState) ->
_ = log_event(
?EV_TRACE,
WoodyState,
#{event => <<?HEADER_E_CLASS/binary, " header missing">>}
),
{result_unexpected, check_error_reason(Headers, 502, WoodyState)};
check_502_error_class(<<"result unexpected">>, Headers, WoodyState) ->
{result_unexpected, check_error_reason(Headers, 502, WoodyState)};
check_502_error_class(<<"resource unavailable">>, Headers, WoodyState) ->
{resource_unavailable, check_error_reason(Headers, 502, WoodyState)};
check_502_error_class(<<"result unknown">>, Headers, WoodyState) ->
{result_unknown, check_error_reason(Headers, 502, WoodyState)};
check_502_error_class(Bad, _, WoodyState) ->
_ = log_internal_error(
?ERROR_RESP_HEADER,
["unknown ", ?HEADER_E_CLASS, " header value: ", Bad],
WoodyState
),
{result_unexpected, ?BAD_RESP_HEADER}.
-spec get_error_class_header_value(http_headers()) ->
header_parse_value().
get_error_class_header_value(Headers) ->
case get_header_value(?HEADER_E_CLASS, Headers) of
None when None =:= none orelse None =:= multiple ->
None;
Value ->
genlib_string:to_lower(Value)
end.
-spec get_header_value(woody:http_header_name(), http_headers()) ->
header_parse_value().
get_header_value(Name, Headers) ->
case lists:dropwhile(fun ({K, _}) -> Name /= genlib_string:to_lower(K) end, Headers) of
[{_, Value} | _] -> Value;
[] -> none
end.
-spec get_body({ok, woody:http_body()} | {error, atom()}, woody_state:st()) ->
{ok, woody:http_body()} | {error, {system, woody_error:system_error()}}.
get_body(B = {ok, _}, _) ->
B;
get_body({error, Reason}, WoodyState) ->
_ = log_internal_error(?ERROR_RESP_BODY, Reason, WoodyState),
{error, {system, {internal, result_unknown, ?ERROR_RESP_BODY}}}.
log_result({ok, Result}, WoodyState) ->
log_event(?EV_SERVICE_RESULT, WoodyState, #{status => ok, result => Result});
log_result({error, {business, ThriftExcept}}, WoodyState) ->
log_event(?EV_SERVICE_RESULT, WoodyState, #{status => ok, class => business, result => ThriftExcept});
log_result({error, Result}, WoodyState) ->
log_event(?EV_SERVICE_RESULT, WoodyState, #{status => error, class => system, result => Result}).
-spec map_result(woody_client:result() | {error, _ThriftError}) ->
woody_client:result().
map_result(Res = {ok, _}) ->
Res;
map_result(Res = {error, {Type, _}}) when Type =:= business orelse Type =:= system ->
Res;
map_result({error, ThriftError}) ->
BinError = woody_error:format_details(ThriftError),
{error, {system, {internal, result_unexpected, <<"client thrift error: ", BinError/binary>>}}}.
log_internal_error(Error, Reason, WoodyState) ->
log_event(?EV_INTERNAL_ERROR, WoodyState, #{error => Error, reason => woody_util:to_binary(Reason)}).
log_event(Event, WoodyState, ExtraMeta) ->
woody_event_handler:handle_event(Event, WoodyState, ExtraMeta).

View File

@ -11,6 +11,7 @@
-export([from_binary/1]).
-export([to_unixtime_ms/1]).
-export([from_unixtime_ms/1]).
-export([unow/0]).
%% Types
-type millisec() :: 0..1000.

View File

@ -35,7 +35,7 @@ format_call(Module, Service, Function, Arguments, Opts) ->
ML = maps:get(max_length, Opts1),
MD = maps:get(max_depth, Opts1),
MPSL = maps:get(max_printable_string_length, Opts1),
Result1 = format_call_(ArgTypes, Arguments, Result0, MD, dec(ML), MPSL, false),
Result1 = format_arguments(ArgTypes, Arguments, 1, Result0, MD, dec(ML), MPSL, false),
<<Result1/binary, ")">>
catch error:badarg ->
Result1 = format_verbatim(Arguments, Result0, Opts1),
@ -43,18 +43,20 @@ format_call(Module, Service, Function, Arguments, Opts) ->
end,
{"~ts", [Result]}.
format_call_([], [], Result, _MD, _ML, _MPSL, _AD) ->
format_arguments([], _, _, Result, _MD, _ML, _MPSL, _AD) ->
Result;
format_call_(_, ArgumentList, Result0, _MD, ML, _MPSL, AD) when byte_size(Result0) > ML ->
HasMoreArguments = AD and (ArgumentList =/= []),
format_arguments(Types, _, _, Result0, _MD, ML, _MPSL, AD) when byte_size(Result0) > ML ->
HasMoreArguments = AD and (Types =/= []),
Result1 = maybe_add_delimiter(HasMoreArguments, Result0),
Result2 = maybe_add_more_marker(HasMoreArguments, Result1),
Result2;
format_call_([Type | RestType], [Argument | RestArgs], Result0, MD, ML, MPSL, AD) ->
format_arguments([Type | RestType], Arguments, I, Result0, MD, ML, MPSL, AD) ->
Argument = element(I, Arguments),
{Result1, AD1} = format_argument(Type, Argument, Result0, MD, ML, MPSL, AD),
format_call_(
format_arguments(
RestType,
RestArgs,
Arguments,
I + 1,
Result1,
MD,
ML,
@ -433,7 +435,7 @@ stop_format(MaybeAddMoreMarker, Result0) ->
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-define(ARGS, [undefined, <<"1CR1Xziml7o">>,
-define(ARGS, {undefined, <<"1CR1Xziml7o">>,
[{contract_modification,
{payproc_ContractModificationUnit, <<"1CR1Y2ZcrA0">>,
{creation,
@ -470,9 +472,9 @@ stop_format(MaybeAddMoreMarker, Result0) ->
{payproc_ShopModificationUnit, <<"1CR1Y2ZcrA2">>,
{shop_account_creation,
{payproc_ShopAccountParams, {domain_CurrencyRef, <<"RUB">>}}}}}]
]).
}).
-define(ARGS2, [{mg_stateproc_CallArgs,
-define(ARGS2, {{mg_stateproc_CallArgs,
{bin,
<<131, 104, 4, 100, 0, 11, 116, 104, 114, 105, 102, 116, 95, 99, 97, 108, 108,
100, 0, 16, 112, 97, 114, 116, 121, 95, 109, 97, 110, 97, 103, 101, 109, 101,
@ -558,7 +560,7 @@ stop_format(MaybeAddMoreMarker, Result0) ->
101, 120, 116, 0, 0, 0, 0, 100, 0, 14, 115, 110, 97, 112,
115, 104, 111, 116, 95, 105, 110, 100, 101, 120, 106>>},
{str, <<"ct">>} =>
{str, <<"application/x-erlang-binary">>}}}}}]
{str, <<"application/x-erlang-binary">>}}}}}}
).
format_msg({Fmt, Params}) ->
@ -945,9 +947,9 @@ depth_and_length_test_() -> [
verbatim_test_() -> [
?_assertEqual(
"PartyManagement:CallMissingFunction("
"[undefined,<<\"1CR1Xziml7o\">>,[{contract_modification,{payproc_ContractModificationUnit,"
"{undefined,<<\"1CR1Xziml7o\">>,[{contract_modification,{payproc_ContractModificationUnit,"
"<<\"1CR1\"...>>,{...}}},{contract_modification,{payproc_ContractModificationUnit,<<...>>,"
"...}},{shop_modification,{payproc_ShopModificationUnit,...}}|...]]"
"...}},{shop_modification,{payproc_ShopModificationUnit,...}}|...]}"
")",
format_msg(
format_call(

View File

@ -441,8 +441,8 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[undefined, <<"1CQdDqPROyW">>,
{payproc_PartyParams, {domain_PartyContactInfo, <<"hg_ct_helper">>}}],
{undefined, <<"1CQdDqPROyW">>,
{payproc_PartyParams, {domain_PartyContactInfo, <<"hg_ct_helper">>}}},
deadline => undefined, execution_start_time => 1565596875497,
function => 'Create',
metadata =>
@ -468,9 +468,9 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[{payproc_UserInfo, <<"1CQdDqPROyW">>,
{{payproc_UserInfo, <<"1CQdDqPROyW">>,
{external_user, {payproc_ExternalUser}}}, undefined,
{payproc_PartyParams, {domain_PartyContactInfo, <<"hg_ct_helper">>}}],
{payproc_PartyParams, {domain_PartyContactInfo, <<"hg_ct_helper">>}}},
deadline => undefined, execution_start_time => 1565596875497,
function => 'Create',
metadata =>
@ -493,7 +493,7 @@ format_service_request_test_() -> [
format_msg_limited(
format_event(
?EV_CALL_SERVICE,
#{args => [undefined, <<"1CQdDqPROyW">>],
#{args => {undefined, <<"1CQdDqPROyW">>},
deadline => undefined, execution_start_time => 1565596875696,
function => 'Get',
metadata =>
@ -516,7 +516,7 @@ format_service_request_test_() -> [
format_msg_limited(
format_event(
?EV_CALL_SERVICE,
#{args => [undefined, <<"~s">>],
#{args => {undefined, <<"~s">>},
deadline => undefined, execution_start_time => 1565596875696,
function => 'Get',
metadata =>
@ -541,9 +541,9 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[{payproc_CustomerParams, <<"1CQdDqPROyW">>, <<"1CQdDwgt3R3">>,
{{payproc_CustomerParams, <<"1CQdDqPROyW">>, <<"1CQdDwgt3R3">>,
{domain_ContactInfo, undefined, <<"invalid_shop">>},
{nl, {json_Null}}}],
{nl, {json_Null}}}},
deadline => undefined,
execution_start_time => 1565596876258,
function => 'Create',
@ -569,9 +569,9 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[{payproc_UserInfo, <<"1CQdDqPROyW">>,
{{payproc_UserInfo, <<"1CQdDqPROyW">>,
{external_user, {payproc_ExternalUser}}},
<<"1CQdDqPROyW">>],
<<"1CQdDqPROyW">>},
deadline => {{{2019, 8, 12}, {8, 1, 46}}, 263},
execution_start_time => 1565596876266, function => 'GetRevision',
metadata =>
@ -596,10 +596,10 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[{payproc_UserInfo, <<"1CQdDqPROyW">>,
{{payproc_UserInfo, <<"1CQdDqPROyW">>,
{external_user, {payproc_ExternalUser}}},
<<"1CQdDqPROyW">>,
{revision, 1}],
{revision, 1}},
deadline => {{{2019, 8, 12}, {8, 1, 46}}, 263},
execution_start_time => 1565596876292,
function => 'Checkout',
@ -623,7 +623,7 @@ format_service_request_test_() -> [
format_msg_limited(
format_event(
?EV_CALL_SERVICE,
#{args => [undefined, <<"1CQdDqPROyW">>, <<>>],
#{args => {undefined, <<"1CQdDqPROyW">>, <<>>},
deadline => undefined,
execution_start_time => 1565596876383,
function => 'Block',
@ -648,7 +648,7 @@ format_service_request_test_() -> [
format_msg_limited(
format_event(
?EV_CALL_SERVICE,
#{args => [undefined, <<"1CQdDqPROyW">>, <<>>],
#{args => {undefined, <<"1CQdDqPROyW">>, <<>>},
deadline => undefined,
execution_start_time => 1565596876458,
function => 'Unblock',
@ -676,7 +676,7 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[{mg_stateproc_SignalArgs,
{{mg_stateproc_SignalArgs,
{init,
{mg_stateproc_InitSignal,
{bin,
@ -686,7 +686,7 @@ format_service_request_test_() -> [
{mg_stateproc_HistoryRange, undefined, undefined, forward},
{mg_stateproc_Content, undefined, {bin, <<>>}},
undefined,
{bin, <<>>}}}],
{bin, <<>>}}}},
deadline => {{{2019, 8, 12}, {12, 46, 36}}, 433},
execution_start_time => 1565613966542,
function => 'ProcessSignal',
@ -723,7 +723,7 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[undefined, <<"1CR1Xziml7o">>,
{undefined, <<"1CR1Xziml7o">>,
[{contract_modification,
{payproc_ContractModificationUnit, <<"1CR1Y2ZcrA0">>,
{creation,
@ -759,7 +759,7 @@ format_service_request_test_() -> [
{shop_modification,
{payproc_ShopModificationUnit, <<"1CR1Y2ZcrA2">>,
{shop_account_creation,
{payproc_ShopAccountParams, {domain_CurrencyRef, <<"RUB">>}}}}}]],
{payproc_ShopAccountParams, {domain_CurrencyRef, <<"RUB">>}}}}}]},
deadline => undefined,
execution_start_time => 1565617299263,
function => 'CreateClaim',
@ -791,7 +791,7 @@ format_service_request_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[{mg_stateproc_CallArgs,
{{mg_stateproc_CallArgs,
{bin,
<<131, 104, 4, 100, 0, 11, 116, 104, 114, 105, 102, 116, 95, 99, 97, 108, 108,
100, 0, 16, 112, 97, 114, 116, 121, 95, 109, 97, 110, 97, 103, 101, 109, 101,
@ -877,7 +877,7 @@ format_service_request_test_() -> [
101, 120, 116, 0, 0, 0, 0, 100, 0, 14, 115, 110, 97, 112,
115, 104, 111, 116, 95, 105, 110, 100, 101, 120, 106>>},
{str, <<"ct">>} =>
{str, <<"application/x-erlang-binary">>}}}}}],
{str, <<"application/x-erlang-binary">>}}}}}},
deadline => {{{2019, 8, 13}, {7, 52, 41}}, 105},
execution_start_time => 1565682731109,
function => 'ProcessCall',
@ -918,7 +918,7 @@ format_service_request_with_limit_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[undefined, <<"1CR1Xziml7o">>,
{undefined, <<"1CR1Xziml7o">>,
[{contract_modification,
{payproc_ContractModificationUnit, <<"1CR1Y2ZcrA0">>,
{creation,
@ -954,7 +954,7 @@ format_service_request_with_limit_test_() -> [
{shop_modification,
{payproc_ShopModificationUnit, <<"1CR1Y2ZcrA2">>,
{shop_account_creation,
{payproc_ShopAccountParams, {domain_CurrencyRef, <<"RUB">>}}}}}]],
{payproc_ShopAccountParams, {domain_CurrencyRef, <<"RUB">>}}}}}]},
deadline => undefined,
execution_start_time => 1565617299263,
function => 'CreateClaim',
@ -1077,10 +1077,10 @@ result_test_() -> [
format_event(
?EV_SERVICE_HANDLER_RESULT,
#{args =>
[{payproc_UserInfo, <<"1CSWG2vduGe">>,
{{payproc_UserInfo, <<"1CSWG2vduGe">>,
{external_user, {payproc_ExternalUser}}},
<<"1CSWG2vduGe">>,
{revision, 6}],
{revision, 6}},
deadline => {{{2019, 8, 13}, {11, 19, 33}}, 42},
execution_start_time => 1565695143068,
function => 'Checkout',
@ -1153,7 +1153,7 @@ result_test_() -> [
format_event(
?EV_SERVICE_HANDLER_RESULT,
#{args =>
[{mg_stateproc_SignalArgs,
{{mg_stateproc_SignalArgs,
{init,
{mg_stateproc_InitSignal,
{bin,
@ -1168,7 +1168,7 @@ result_test_() -> [
{mg_stateproc_HistoryRange, undefined, undefined, forward},
{mg_stateproc_Content, undefined, {bin, <<>>}},
undefined,
{bin, <<>>}}}],
{bin, <<>>}}}},
deadline => {{{2019, 8, 13}, {11, 19, 33}}, 606},
execution_start_time => 1565695143707, function => 'ProcessSignal',
metadata =>
@ -1216,7 +1216,7 @@ result_test_() -> [
format_event(
?EV_CALL_SERVICE,
#{args =>
[{mg_stateproc_SignalArgs,
{{mg_stateproc_SignalArgs,
{init,
{mg_stateproc_InitSignal,
{bin,
@ -1231,7 +1231,7 @@ result_test_() -> [
{mg_stateproc_HistoryRange, undefined, undefined, forward},
{mg_stateproc_Content, undefined, {bin, <<>>}},
undefined,
{bin, <<>>}}}],
{bin, <<>>}}}},
deadline => {{{2019, 8, 13}, {11, 19, 33}}, 606},
execution_start_time => 1565695143707, function => 'ProcessSignal',
metadata =>
@ -1309,7 +1309,7 @@ exception_test_() -> [
format_msg_limited(
format_event(
?EV_SERVICE_RESULT,
#{args => [<<"1Cfo5OJzx6O">>],
#{args => {<<"1Cfo5OJzx6O">>},
deadline => undefined,
execution_start_time => 1566386841317,
class => business,
@ -1332,7 +1332,7 @@ exception_test_() -> [
format_msg_limited(
format_event(
?EV_SERVICE_RESULT,
#{args => [
#{args => {
undefined,
<<"1Cfo9igJRS4">>,
{payproc_InvoicePaymentParams, {payment_resource, {payproc_PaymentResourcePayerParams,
@ -1340,7 +1340,7 @@ exception_test_() -> [
visa, <<"424242">>, <<"4242">>, undefined, undefined, undefined, undefined, undefined}},
<<"SESSION42">>, {domain_ClientInfo, undefined, undefined}},
{domain_ContactInfo, undefined, undefined}}},
{instant, {payproc_InvoicePaymentParamsFlowInstant}}, true, undefined, undefined, undefined}],
{instant, {payproc_InvoicePaymentParamsFlowInstant}}, true, undefined, undefined, undefined}},
deadline => undefined, execution_start_time => 1566386899959,
class => business,
function => 'StartPayment',
@ -1361,7 +1361,7 @@ exception_test_() -> [
format_msg_limited(
format_event(
?EV_SERVICE_RESULT,
#{args=>[
#{args=>{
undefined,
<<"1FToOuf532G">>,
{payproc_InvoicePaymentParams,
@ -1369,7 +1369,7 @@ exception_test_() -> [
{domain_RecurrentParentPayment, <<"1FToOLnG2Ou">>, <<"1">>},
{domain_ContactInfo, undefined, undefined}}},
{instant, {payproc_InvoicePaymentParamsFlowInstant}},
true, undefined, undefined, undefined, undefined}],
true, undefined, undefined, undefined, undefined}},
deadline => undefined,
execution_start_time => 1575444908463,
class => business,

View File

@ -7,7 +7,16 @@
-export([child_spec/2]).
%% Types
-type options() :: woody_server_thrift_http_handler:options().
-type options() :: #{
event_handler := woody:ev_handlers(),
protocol => thrift,
transport => http,
%% Set to override protocol handler module selection, useful for test purposes, rarely
%% if ever needed otherwise.
protocol_handler_override => module(),
%% Implementation-specific options
_ => _
}.
-export_type([options/0]).
%% Behaviour definition

View File

@ -3,6 +3,9 @@
%% API
-export([init_handler/3, invoke_handler/1]).
%% Behaviour dispatch
-export([handle_function/4]).
-include_lib("thrift/include/thrift_constants.hrl").
-include_lib("thrift/include/thrift_protocol.hrl").
-include("woody_defs.hrl").
@ -72,6 +75,13 @@ invoke_handler(State) ->
{_, {ok, Reply}} = thrift_protocol:close_transport(Proto),
handle_result(Result, Reply, MsgType).
-spec handle_function(woody:handler(woody:options()), woody:func(), woody:args(), woody_state:st()) ->
{ok, woody:result()} | no_return().
handle_function(Handler, Function, Args, WoodyState) ->
_ = woody_event_handler:handle_event(?EV_INVOKE_SERVICE_HANDLER, WoodyState, #{}),
{Module, Opts} = woody_util:get_mod_opts(Handler),
Module:handle_function(Function, Args, woody_state:get_context(WoodyState), Opts).
%%
%% Internal functions
%%
@ -151,8 +161,7 @@ add_ev_meta(WoodyState, Service = {_, ServiceName}, Function, ReplyType) ->
decode_request(State = #{th_proto := Proto, th_param_type := ParamsType, woody_state := WoodyState}) ->
case thrift_protocol:read(Proto, ParamsType) of
{Proto1, {ok, Args}} ->
Args1 = tuple_to_list(Args),
State#{th_proto => Proto1, args => Args1, woody_state := add_ev_meta(WoodyState, Args1)};
State#{th_proto => Proto1, args => Args, woody_state := add_ev_meta(WoodyState, Args)};
{_, {error, Error}} ->
throw_decode_error(Error)
end.
@ -212,9 +221,7 @@ call_handler(#{
function := Function,
args := Args})
->
_ = woody_event_handler:handle_event(?EV_INVOKE_SERVICE_HANDLER, WoodyState, #{}),
{Module, Opts} = woody_util:get_mod_opts(Handler),
Module:handle_function(Function, Args, woody_state:get_context(WoodyState), Opts).
handle_function(Handler, Function, Args, WoodyState).
-spec handle_success({ok, woody:result()}, state()) ->
{ok | {error, {system, woody_error:system_error()}}, state()}.

View File

@ -17,10 +17,6 @@
-export([init/2]).
-export([terminate/3]).
%% API for woody_stream_handler
-export([trace_req/4]).
-export([trace_resp/6]).
%% Types
-type handler_limits() :: #{
max_heap_size => integer() %% process words, see erlang:process_flag(max_heap_size, MaxHeapSize) for details.
@ -71,6 +67,7 @@
-type route_opts() :: #{
handlers := list(woody:http_handler(woody:th_handler())),
event_handler := woody:ev_handlers(),
read_body_opts => read_body_opts(),
protocol => thrift,
transport => http,
handler_limits => handler_limits()
@ -82,13 +79,12 @@
-export_type([protocol_opts/0]).
-type server_opts() :: #{regexp_meta => re_mp()}.
-type state() :: #{
th_handler := woody:th_handler(),
th_handler => woody:th_handler(),
ev_handler := woody:ev_handlers(),
server_opts := server_opts(),
read_body_opts := read_body_opts(),
handler_limits := handler_limits(),
regexp_meta := re_mp(),
url => woody:url(),
woody_state => woody_state:st()
}.
@ -151,9 +147,9 @@ get_cowboy_config(Opts = #{event_handler := EvHandler}) ->
Dispatch = get_dispatch(Opts),
ProtocolOpts = maps:get(protocol_opts, Opts, #{}),
CowboyOpts = maps:put(stream_handlers, [woody_monitor_h, woody_trace_h, cowboy_stream_h], ProtocolOpts),
ReadBodyOpts = maps:get(read_body_opts, Opts, #{}),
Env = woody_trace_h:env(maps:with([event_handler], Opts)),
maps:merge(#{
env =>#{dispatch => Dispatch, event_handler => EvHandler, read_body_opts => ReadBodyOpts},
env => Env#{dispatch => Dispatch},
max_header_name_length => 64
}, CowboyOpts).
@ -175,35 +171,32 @@ get_dispatch(Opts) ->
-spec get_all_routes(options())->
[route(_)].
get_all_routes(Opts) ->
RouteOpts = maps:with([handlers, event_handler, read_body_opts, handler_limits, protocol, transport], Opts),
AdditionalRoutes = maps:get(additional_routes, Opts, []),
AdditionalRoutes ++ get_routes(maps:with([handlers, event_handler, handler_limits, protocol, transport], Opts)).
AdditionalRoutes ++ get_routes(RouteOpts).
-spec get_routes(route_opts())->
-spec get_routes(route_opts()) ->
[route(state())].
get_routes(Opts = #{handlers := Handlers, event_handler := EvHandler}) ->
Limits = maps:get(handler_limits, Opts, #{}),
get_routes(config(), Limits, EvHandler, Handlers, []).
get_routes(Opts = #{handlers := Handlers}) ->
State0 = init_state(Opts),
[get_route(State0, Handler) || Handler <- Handlers].
-spec get_routes(server_opts(), handler_limits(), woody:ev_handlers(), Handlers, Routes) ->
Routes when Handlers :: list(woody:http_handler(woody:th_handler())), Routes :: [route(state())].
get_routes(_, _, _, [], Routes) ->
Routes;
get_routes(ServerOpts, Limits, EvHandler, [{PathMatch, {Service, Handler}} | T], Routes) ->
get_routes(ServerOpts, Limits, EvHandler, T, [
{PathMatch, ?MODULE, #{
th_handler => {Service, Handler},
ev_handler => EvHandler,
server_opts => ServerOpts,
handler_limits => Limits
}} | Routes
]);
get_routes(_, _, _, [Handler | _], _) ->
-spec get_route(state(), woody:http_handler(woody:th_handler())) ->
route(state()).
get_route(State0, {PathMatch, {Service, Handler}}) ->
{PathMatch, ?MODULE, State0#{th_handler => {Service, Handler}}};
get_route(_, Handler) ->
error({bad_handler_spec, Handler}).
-spec config() ->
server_opts().
config() ->
#{regexp_meta => compile_filter_meta()}.
-spec init_state(route_opts()) ->
state().
init_state(Opts = #{}) ->
#{
ev_handler => maps:get(event_handler, Opts),
read_body_opts => maps:get(read_body_opts, Opts, #{}),
handler_limits => maps:get(handler_limits, Opts, #{}),
regexp_meta => compile_filter_meta()
}.
-spec compile_filter_meta() ->
re_mp().
@ -211,46 +204,6 @@ compile_filter_meta() ->
{ok, Re} = re:compile(?HEADER_META_RE, [unicode, caseless]),
Re.
-spec trace_req(true, cowboy_req:req(), woody:ev_handlers(), server_opts()) ->
cowboy_req:req().
trace_req(true, Req, EvHandler, ServerOpts) ->
Url = unicode:characters_to_binary(cowboy_req:uri(Req)),
Headers = cowboy_req:headers(Req),
Meta = #{
role => server,
event => <<"http request received">>,
url => Url,
headers => Headers
},
Meta1 = case get_body(Req, ServerOpts) of
{ok, Body, _} ->
Meta#{body => Body, body_status => ok}
end,
_ = woody_event_handler:handle_event(EvHandler, ?EV_TRACE, undefined, Meta1),
Req;
trace_req(_, Req, _, _) ->
Req.
-spec trace_resp(
true,
cowboy_req:req(),
woody:http_code(),
woody:http_headers(),
woody:http_body(),
woody:ev_handlers()
) ->
cowboy_req:req().
trace_resp(true, Req, Code, Headers, Body, EvHandler) ->
_ = woody_event_handler:handle_event(EvHandler, ?EV_TRACE, undefined, #{
role => server,
event => <<"http response send">>,
code => Code,
headers => Headers,
body => Body}),
Req;
trace_resp(_, Req, _, _, _, _) ->
Req.
%%
%% cowboy_http_handler callbacks
%%
@ -286,10 +239,10 @@ set_handler_limits(Limits) ->
handle(Req, State = #{
url := Url,
woody_state := WoodyState,
server_opts := ServerOpts,
read_body_opts := ReadBodyOpts,
th_handler := ThriftHandler
}) ->
Req2 = case get_body(Req, ServerOpts) of
Req2 = case get_body(Req, ReadBodyOpts) of
{ok, Body, Req1} when byte_size(Body) > 0 ->
_ = woody_event_handler:handle_event(?EV_SERVER_RECEIVE, WoodyState, #{url => Url, status => ok}),
handle_request(Body, ThriftHandler, WoodyState, Req1);
@ -433,13 +386,13 @@ check_deadline(Deadline, Req, State = #{url := Url, woody_state := WoodyState})
-spec check_metadata_headers(woody:http_headers(), cowboy_req:req(), state()) ->
check_result().
check_metadata_headers(Headers, Req, State = #{woody_state := WoodyState, server_opts := ServerOpts}) ->
WoodyState1 = set_metadata(find_metadata(Headers, ServerOpts), WoodyState),
check_metadata_headers(Headers, Req, State = #{woody_state := WoodyState, regexp_meta := ReMeta}) ->
WoodyState1 = set_metadata(find_metadata(Headers, ReMeta), WoodyState),
{ok, Req, update_woody_state(State, WoodyState1, Req)}.
-spec find_metadata(woody:http_headers(), server_opts()) ->
-spec find_metadata(woody:http_headers(), re_mp()) ->
woody_context:meta().
find_metadata(Headers, #{regexp_meta := Re}) ->
find_metadata(Headers, Re) ->
RpcId = ?HEADER_RPC_ID,
RootId = ?HEADER_RPC_ROOT_ID,
ParentId = ?HEADER_RPC_PARENT_ID,
@ -493,12 +446,10 @@ reply_client_error(Code, Reason, Req, #{url := Url, woody_state := WoodyState})
reply(Code, set_error_headers(<<"Result Unexpected">>, Reason, Req), WoodyState).
%% handle functions
-spec get_body(cowboy_req:req(), server_opts()) ->
-spec get_body(cowboy_req:req(), read_body_opts()) ->
{ok, woody:http_body(), cowboy_req:req()}.
get_body(Req, #{read_body_opts := ReadBodyOpts}) ->
do_get_body(<<>>, Req, ReadBodyOpts);
get_body(Req, _) ->
do_get_body(<<>>, Req, #{}).
get_body(Req, ReadBodyOpts) ->
do_get_body(<<>>, Req, ReadBodyOpts).
do_get_body(Body, Req, Opts) ->
case cowboy_req:read_body(Req, Opts) of

View File

@ -0,0 +1,713 @@
-module(woody_server_thrift_v2).
-behaviour(woody_server).
-dialyzer(no_undefined_callbacks).
-include("woody_defs.hrl").
%% woody_server callback
-export([child_spec/2]).
%% API
-export([get_routes/1]).
%% cowboy_handler callbacks
-behaviour(cowboy_handler).
-export([init/2]).
-export([terminate/3]).
%% Types
-type handler_limits() :: #{
max_heap_size => integer() %% process words, see erlang:process_flag(max_heap_size, MaxHeapSize) for details.
}.
-export_type([handler_limits/0]).
-type transport_opts() :: #{
connection_type => worker | supervisor,
handshake_timeout => timeout(),
max_connections => ranch:max_conns(),
logger => module(),
num_acceptors => pos_integer(),
shutdown => timeout() | brutal_kill,
socket => any(),
socket_opts => any(),
transport => module() % ranch_tcp | ranch_ssl
}.
-export_type([transport_opts/0]).
-type route(T) :: {woody:path(), module(), T}.
-export_type([route/1]).
-type read_body_opts() :: cowboy_req:read_body_opts().
%% ToDo: restructure options() to split server options and route options and
%% get rid of separate route_opts() when backward compatibility isn't an issue.
-type options() :: #{
handlers := list(woody:http_handler(woody:th_handler())),
event_handler := woody:ev_handlers(),
ip := inet:ip_address(),
port := inet:port_number(),
protocol => thrift,
transport => http,
transport_opts => transport_opts(),
read_body_opts => read_body_opts(),
protocol_opts => protocol_opts(),
handler_limits => handler_limits(),
additional_routes => [route(_)],
%% shutdown_timeout: time to drain current connections when shutdown signal is recieved
%% NOTE: when changing this value make sure to take into account the request_timeout and
%% max_keepalive settings of protocol_opts() to achieve the desired effect
shutdown_timeout => timeout()
}.
-export_type([options/0]).
-type route_opts() :: #{
handlers := list(woody:http_handler(woody:th_handler())),
event_handler := woody:ev_handlers(),
protocol => thrift,
transport => http,
read_body_opts => read_body_opts(),
handler_limits => handler_limits()
}.
-export_type([route_opts/0]).
-define(ROUTE_OPT_NAMES, [
handlers,
event_handler,
protocol,
transport,
read_body_opts,
handler_limits
]).
-type re_mp() :: tuple().
-type protocol_opts() :: cowboy_http:opts().
-export_type([protocol_opts/0]).
-type state() :: #{
th_handler => woody:th_handler(),
ev_handler := woody:ev_handlers(),
read_body_opts := read_body_opts(),
handler_limits := handler_limits(),
regexp_meta := re_mp(),
url => woody:url(),
woody_state => woody_state:st()
}.
-type cowboy_init_result() ::
{ok, cowboy_req:req(), state() | undefined}
| {module(), cowboy_req:req(), state() | undefined, any()}.
-type check_result() ::
{ok, cowboy_req:req(), state() | undefined}
| {stop, cowboy_req:req(), undefined}.
-define(DEFAULT_ACCEPTORS_POOLSIZE, 100).
-define(DEFAULT_SHUTDOWN_TIMEOUT, 0).
%% nginx should be configured to take care of various limits.
-define(DUMMY_REQ_ID, <<"undefined">>).
%%
%% woody_server callback
%%
-spec child_spec(atom(), options()) ->
supervisor:child_spec().
child_spec(Id, Opts) ->
{Transport, TransportOpts} = get_socket_transport(Opts),
CowboyOpts = get_cowboy_config(Opts),
RanchRef = {?MODULE, Id},
DrainSpec = make_drain_childspec(RanchRef, Opts),
RanchSpec = ranch:child_spec(RanchRef, Transport, TransportOpts, cowboy_clear, CowboyOpts),
make_server_childspec(Id, [RanchSpec, DrainSpec]).
make_drain_childspec(Ref, Opts) ->
ShutdownTimeout = maps:get(shutdown_timeout, Opts, ?DEFAULT_SHUTDOWN_TIMEOUT),
DrainOpts = #{shutdown => ShutdownTimeout, ranch_ref => Ref},
woody_server_http_drainer:child_spec(DrainOpts).
make_server_childspec(Id, Children) ->
Flags = #{strategy => one_for_all},
#{
id => Id,
start => {genlib_adhoc_supervisor, start_link, [Flags, Children]},
type => supervisor
}.
get_socket_transport(Opts = #{ip := Ip, port := Port}) ->
Defaults = #{num_acceptors => ?DEFAULT_ACCEPTORS_POOLSIZE},
TransportOpts = maps:merge(Defaults, maps:get(transport_opts, Opts, #{})),
Transport = maps:get(transport, TransportOpts, ranch_tcp),
SocketOpts = [{ip, Ip}, {port, Port} | maps:get(socket_opts, TransportOpts, [])],
{Transport, set_ranch_option(socket_opts, SocketOpts, TransportOpts)}.
set_ranch_option(Key, Value, Opts) ->
Opts#{Key => Value}.
-spec get_cowboy_config(options()) ->
protocol_opts().
get_cowboy_config(Opts = #{event_handler := EvHandler}) ->
ok = validate_event_handler(EvHandler),
Dispatch = get_dispatch(Opts),
ProtocolOpts = maps:get(protocol_opts, Opts, #{}),
CowboyOpts = maps:put(stream_handlers, [woody_monitor_h, woody_trace_h, cowboy_stream_h], ProtocolOpts),
Env = woody_trace_h:env(maps:with([event_handler], Opts)),
maps:merge(#{
env => Env#{dispatch => Dispatch},
max_header_name_length => 64
}, CowboyOpts).
validate_event_handler(Handlers) when is_list(Handlers) ->
true = lists:all(
fun(Handler) ->
is_tuple(woody_util:get_mod_opts(Handler))
end, Handlers),
ok;
validate_event_handler(Handler) ->
validate_event_handler([Handler]).
-spec get_dispatch(options())->
cowboy_router:dispatch_rules().
get_dispatch(Opts) ->
cowboy_router:compile([{'_', get_all_routes(Opts)}]).
-spec get_all_routes(options())->
[route(_)].
get_all_routes(Opts) ->
AdditionalRoutes = maps:get(additional_routes, Opts, []),
AdditionalRoutes ++ get_routes(maps:with(?ROUTE_OPT_NAMES, Opts)).
-spec get_routes(route_opts()) ->
[route(state())].
get_routes(Opts = #{handlers := Handlers}) ->
State0 = init_state(Opts),
[get_route(State0, Handler) || Handler <- Handlers].
-spec get_route(state(), woody:http_handler(woody:th_handler())) ->
route(state()).
get_route(State0, {PathMatch, {Service, Handler}}) ->
{PathMatch, ?MODULE, State0#{th_handler => {Service, Handler}}};
get_route(_, Handler) ->
error({bad_handler_spec, Handler}).
-spec init_state(route_opts()) ->
state().
init_state(Opts = #{}) ->
#{
ev_handler => maps:get(event_handler, Opts),
read_body_opts => maps:get(read_body_opts, Opts, #{}),
handler_limits => maps:get(handler_limits, Opts, #{}),
regexp_meta => compile_filter_meta()
}.
-spec compile_filter_meta() ->
re_mp().
compile_filter_meta() ->
{ok, Re} = re:compile(?HEADER_META_RE, [unicode, caseless]),
Re.
%%
%% cowboy_http_handler callbacks
%%
-spec init(cowboy_req:req(), state()) ->
cowboy_init_result().
init(Req, Opts = #{ev_handler := EvHandler, handler_limits := Limits}) ->
ok = set_handler_limits(Limits),
Url = unicode:characters_to_binary(cowboy_req:uri(Req)),
WoodyState = create_dummy_state(EvHandler),
Opts1 = update_woody_state(Opts#{url => Url}, WoodyState, Req),
case check_request(Req, Opts1) of
{ok, Req1, State} -> handle(Req1, State);
{stop, Req1, State} -> {ok, Req1, State}
end.
-spec set_handler_limits(handler_limits()) ->
ok.
set_handler_limits(Limits) ->
case maps:get(max_heap_size, Limits, undefined) of
undefined ->
ok;
MaxHeapSize ->
_ = erlang:process_flag(max_heap_size, #{
size => MaxHeapSize,
kill => true,
error_logger => true
}),
ok
end.
-spec handle(cowboy_req:req(), state()) ->
{ok, cowboy_req:req(), _}.
handle(Req, State = #{
url := Url,
woody_state := WoodyState,
read_body_opts := ReadBodyOpts,
th_handler := ThriftHandler
}) ->
Req2 = case get_body(Req, ReadBodyOpts) of
{ok, Body, Req1} when byte_size(Body) > 0 ->
_ = handle_event(?EV_SERVER_RECEIVE, WoodyState, #{url => Url, status => ok}),
handle_request(Body, ThriftHandler, WoodyState, Req1);
{ok, <<>>, Req1} ->
reply_client_error(400, <<"body empty">>, Req1, State)
end,
{ok, Req2, undefined}.
create_dummy_state(EvHandler) ->
DummyRpcID = #{
span_id => ?DUMMY_REQ_ID,
trace_id => ?DUMMY_REQ_ID,
parent_id => ?DUMMY_REQ_ID
},
woody_state:new(server, woody_context:new(DummyRpcID), EvHandler).
-spec terminate(_Reason, _Req, state() | _) ->
ok.
terminate(normal, _Req, _Status) ->
ok;
terminate(Reason, _Req, #{ev_handler := EvHandler} = Opts) ->
WoodyState = maps:get(woody_state, Opts, create_dummy_state(EvHandler)),
_ = woody_event_handler:handle_event(?EV_INTERNAL_ERROR, WoodyState, #{
error => <<"http handler terminated abnormally">>,
reason => woody_error:format_details(Reason),
class => undefined,
final => true
}),
ok.
%% init functions
-define(CODEC, thrift_strict_binary_codec).
%% First perform basic http checks: method, content type, etc,
%% then check woody related headers: IDs, deadline, meta.
-spec check_request(cowboy_req:req(), state()) ->
check_result().
check_request(Req, State) ->
check_method(cowboy_req:method(Req), Req, State).
-spec check_method(woody:http_header_val(), cowboy_req:req(), state()) ->
check_result().
check_method(<<"POST">>, Req, State) ->
check_content_type(cowboy_req:header(<<"content-type">>, Req), Req, State);
check_method(Method, Req, State) ->
Req1 = cowboy_req:set_resp_header(<<"allow">>, <<"POST">>, Req),
Reason = woody_util:to_binary(["wrong method: ", Method]),
reply_bad_header(405, Reason, Req1, State).
-spec check_content_type(woody:http_header_val() | undefined, cowboy_req:req(), state()) ->
check_result().
check_content_type(?CONTENT_TYPE_THRIFT, Req, State) ->
Header = cowboy_req:header(<<"accept">>, Req),
check_accept(Header, Req, State);
check_content_type(BadCType, Req, State) ->
reply_bad_header(415, woody_util:to_binary(["wrong content type: ", BadCType]), Req, State).
-spec check_accept(woody:http_header_val() | undefined, cowboy_req:req(), state()) ->
check_result().
check_accept(Accept, Req, State) when
Accept =:= ?CONTENT_TYPE_THRIFT ;
Accept =:= undefined
->
check_woody_headers(Req, State);
check_accept(BadAccept, Req1, State) ->
reply_bad_header(406, woody_util:to_binary(["wrong client accept: ", BadAccept]), Req1, State).
-spec check_woody_headers(cowboy_req:req(), state()) ->
check_result().
check_woody_headers(Req, State = #{woody_state := WoodyState0}) ->
case get_rpc_id(Req) of
{ok, RpcId, Req1} ->
WoodyState1 = set_cert(Req1, set_rpc_id(RpcId, WoodyState0)),
check_deadline_header(
cowboy_req:header(?HEADER_DEADLINE, Req1),
Req1,
update_woody_state(State, WoodyState1, Req1)
);
{error, BadRpcId, Req1} ->
WoodyState1 = set_rpc_id(BadRpcId, WoodyState0),
reply_bad_header(400, woody_util:to_binary(["bad ", ?HEADER_PREFIX, " id header"]),
Req1, update_woody_state(State, WoodyState1, Req1)
)
end.
-spec get_rpc_id(cowboy_req:req()) ->
{ok | error, woody:rpc_id(), cowboy_req:req()}.
get_rpc_id(Req) ->
check_ids(maps:fold(
fun get_rpc_id/3,
#{req => Req},
#{
span_id => ?HEADER_RPC_ID,
trace_id => ?HEADER_RPC_ROOT_ID,
parent_id => ?HEADER_RPC_PARENT_ID
}
)).
get_rpc_id(Id, Header, Acc = #{req := Req}) ->
case cowboy_req:header(Header, Req) of
undefined ->
Acc#{Id => ?DUMMY_REQ_ID, req => Req, status => error};
IdVal ->
Acc#{Id => IdVal, req => Req}
end.
check_ids(Map = #{status := error, req := Req}) ->
{error, maps:without([req, status], Map), Req};
check_ids(Map = #{req := Req}) ->
{ok, maps:without([req], Map), Req}.
-spec check_deadline_header(Header, Req, state()) -> cowboy_init_result() when
Header :: woody:http_header_val() | undefined, Req :: cowboy_req:req().
check_deadline_header(undefined, Req, State) ->
check_metadata_headers(cowboy_req:headers(Req), Req, State);
check_deadline_header(DeadlineBin, Req, State) ->
try woody_deadline:from_binary(DeadlineBin) of
Deadline -> check_deadline(Deadline, Req, State)
catch
error:{bad_deadline, Error} ->
ErrorDescription = woody_util:to_binary(["bad ", ?HEADER_DEADLINE, " header: ", Error]),
reply_bad_header(400, ErrorDescription, Req, State)
end.
-spec check_deadline(woody:deadline(), cowboy_req:req(), state()) ->
check_result().
check_deadline(Deadline, Req, State = #{url := Url, woody_state := WoodyState}) ->
case woody_deadline:is_reached(Deadline) of
true ->
_ = handle_event(
?EV_SERVER_RECEIVE,
WoodyState,
#{url => Url, status => error, reason => <<"Deadline reached">>}
),
Req1 = handle_error({system, {internal, resource_unavailable, <<"deadline reached">>}}, Req, WoodyState),
{stop, Req1, undefined};
false ->
WoodyState1 = set_deadline(Deadline, WoodyState),
Headers = cowboy_req:headers(Req),
check_metadata_headers(Headers, Req, update_woody_state(State, WoodyState1, Req))
end.
-spec check_metadata_headers(woody:http_headers(), cowboy_req:req(), state()) ->
check_result().
check_metadata_headers(Headers, Req, State = #{woody_state := WoodyState, regexp_meta := ReMeta}) ->
WoodyState1 = set_metadata(find_metadata(Headers, ReMeta), WoodyState),
{ok, Req, update_woody_state(State, WoodyState1, Req)}.
-spec find_metadata(woody:http_headers(), re_mp()) ->
woody_context:meta().
find_metadata(Headers, Re) ->
RpcId = ?HEADER_RPC_ID,
RootId = ?HEADER_RPC_ROOT_ID,
ParentId = ?HEADER_RPC_PARENT_ID,
maps:fold(
fun(H, V, Acc) when
H =/= RpcId andalso
H =/= RootId andalso
H =/= ParentId
->
case re:replace(H, Re, "", [{return, binary}, anchored]) of
H -> Acc;
MetaHeader -> Acc#{MetaHeader => V}
end;
(_, _, Acc) -> Acc
end,
#{}, Headers).
-spec set_rpc_id(woody:rpc_id(), woody_state:st()) ->
woody_state:st().
set_rpc_id(RpcId, WoodyState) ->
woody_state:update_context(woody_context:new(RpcId), WoodyState).
-spec set_cert(cowboy_req:req(), woody_state:st()) ->
woody_state:st().
set_cert(Req, WoodyState) ->
Cert = woody_cert:from_req(Req),
Context = woody_state:get_context(WoodyState),
woody_state:update_context(woody_context:set_cert(Cert, Context), WoodyState).
-spec set_deadline(woody:deadline(), woody_state:st()) ->
woody_state:st().
set_deadline(Deadline, WoodyState) ->
woody_state:add_context_deadline(Deadline, WoodyState).
-spec set_metadata(woody_context:meta(), woody_state:st()) ->
woody_state:st().
set_metadata(Meta, WoodyState) ->
woody_state:add_context_meta(Meta, WoodyState).
-spec reply_bad_header(woody:http_code(), woody:http_header_val(), cowboy_req:req(), state()) ->
{stop, cowboy_req:req(), undefined}.
reply_bad_header(Code, Reason, Req, State) when is_integer(Code) ->
Req1 = reply_client_error(Code, Reason, Req, State),
{stop, Req1, undefined}.
-spec reply_client_error(woody:http_code(), woody:http_header_val(), cowboy_req:req(), state()) ->
cowboy_req:req().
reply_client_error(Code, Reason, Req, #{url := Url, woody_state := WoodyState}) ->
_ = handle_event(
?EV_SERVER_RECEIVE,
WoodyState,
#{url => Url, status => error, reason => Reason}
),
reply(Code, set_error_headers(<<"Result Unexpected">>, Reason, Req), WoodyState).
-spec get_body(cowboy_req:req(), read_body_opts()) ->
{ok, woody:http_body(), cowboy_req:req()}.
get_body(Req, Opts) ->
do_get_body(<<>>, Req, Opts).
do_get_body(Body, Req, Opts) ->
case cowboy_req:read_body(Req, Opts) of
{ok, Body1, Req1} ->
{ok, <<Body/binary, Body1/binary>>, Req1};
{more, Body1, Req1} ->
do_get_body(<<Body/binary, Body1/binary>>, Req1, Opts)
end.
-spec handle_request(woody:http_body(), woody:th_handler(), woody_state:st(), cowboy_req:req()) ->
cowboy_req:req().
handle_request(Body, ThriftHandler = {Service, _}, WoodyState, Req) ->
ok = woody_monitor_h:set_event(?EV_SERVICE_HANDLER_RESULT, Req),
Buffer = ?CODEC:new(Body),
case thrift_processor_codec:read_function_call(Buffer, ?CODEC, Service) of
{ok, SeqId, Invocation, Leftovers} ->
case ?CODEC:close(Leftovers) of
<<>> ->
handle_invocation(SeqId, Invocation, ThriftHandler, Req, WoodyState);
Bytes ->
handle_decode_error({excess_response_body, Bytes, Invocation}, Req, WoodyState)
end;
{error, Reason} ->
handle_decode_error(Reason, Req, WoodyState)
end.
-spec handle_decode_error(_Reason, cowboy_req:req(), woody_state:st()) ->
cowboy_req:req().
handle_decode_error(Reason, Req, WoodyState) ->
_ = handle_event(
?EV_INTERNAL_ERROR,
WoodyState,
#{
error => <<"thrift protocol read failed">>,
reason => woody_error:format_details(Reason)
}
),
handle_error(client_error(Reason), Req, WoodyState).
-spec client_error(_Reason) ->
{client, woody_error:details()}.
client_error({bad_binary_protocol_version, Version}) ->
BinVersion = genlib:to_binary(Version),
{client, <<"thrift: bad binary protocol version: ", BinVersion/binary>>};
client_error(no_binary_protocol_version) ->
{client, <<"thrift: no binary protocol version">>};
client_error({bad_function_name, FName}) ->
case binary:match(FName, <<":">>) of
nomatch ->
{client, woody_util:to_binary([<<"thrift: unknown function: ">>, FName])};
_ ->
{client, <<"thrift: multiplexing (not supported)">>}
end;
client_error(Reason) ->
{client, woody_util:to_binary([<<"thrift decode error: ">>, woody_error:format_details(Reason)])}.
-spec handle_invocation(integer(), Invocation, woody:th_handler(), cowboy_req:req(), woody_state:st()) ->
cowboy_req:req() when Invocation :: {call | oneway, woody:func(), woody:args()}.
handle_invocation(SeqId, {ReplyType, Function, Args}, {Service, Handler}, Req, WoodyState) ->
WoodyState1 = add_ev_meta(WoodyState, Service, ReplyType, Function, Args),
case ReplyType of
call ->
Result = handle_call(Handler, Service, Function, Args, WoodyState1),
handle_result(Result, Service, Function, SeqId, Req, WoodyState1);
oneway ->
Req1 = reply(200, Req, WoodyState1),
_Result = handle_call(Handler, Service, Function, Args, WoodyState1),
Req1
end.
-type call_result() ::
ok |
{reply, woody:result()} |
{exception, _TypeName, _Exception} |
{error, {system, woody_error:system_error()}}.
-spec handle_call(woody:handler(_), woody:service(), woody:func(), woody:args(), woody_state:st()) ->
call_result().
handle_call(Handler, Service, Function, Args, WoodyState) ->
try
Result = call_handler(Handler, Function, Args, WoodyState),
_ = handle_event(
?EV_SERVICE_HANDLER_RESULT,
WoodyState,
#{status => ok, result => Result}
),
case Result of
{ok, ok} -> ok;
{ok, Reply} -> {reply, Reply}
end
catch
throw:Exception:Stack ->
process_handler_throw(Exception, Stack, Service, Function, WoodyState);
Class:Reason:Stack ->
process_handler_error(Class, Reason, Stack, WoodyState)
end.
-spec call_handler(woody:handler(_), woody:func(), woody:args(), woody_state:st()) ->
{ok, woody:result()} | no_return().
call_handler(Handler, Function, Args, WoodyState) ->
woody_server_thrift_handler:handle_function(Handler, Function, Args, WoodyState).
-spec process_handler_throw(_Exception, woody_error:stack(), woody:service(), woody:func(), woody_state:st()) ->
{exception, _TypeName, _Exception} |
{error, {system, woody_error:system_error()}}.
process_handler_throw(Exception, Stack, Service, Function, WoodyState) ->
case thrift_processor_codec:match_exception(Service, Function, Exception) of
{ok, TypeName} ->
_ = handle_event(
?EV_SERVICE_HANDLER_RESULT,
WoodyState,
#{status => error, class => business, result => Exception}
),
{exception, TypeName, Exception};
{error, _} ->
process_handler_error(throw, Exception, Stack, WoodyState)
end.
-spec process_handler_error(_Class :: atom(), _Reason, woody_error:stack(), woody_state:st()) ->
{error, {system, woody_error:system_error()}}.
process_handler_error(error, {woody_error, Error = {_, _, _}}, _Stack, WoodyState) ->
_ = handle_event(
?EV_SERVICE_HANDLER_RESULT,
WoodyState,
#{status => error, class => system, result => Error}
),
{error, {system, Error}};
process_handler_error(Class, Reason, Stack, WoodyState) ->
_ = handle_event(
?EV_SERVICE_HANDLER_RESULT,
WoodyState,
#{status => error, class => system, result => Reason, except_class => Class, stack => Stack}
),
Error = {internal, result_unexpected, format_unexpected_error(Class, Reason, Stack)},
{error, {system, Error}}.
-spec handle_result(call_result(), woody:service(), woody:func(), integer(), Req, woody_state:st()) ->
Req when Req :: cowboy_req:req().
handle_result(Res, Service, Function, SeqId, Req, WoodyState) when
Res == ok; element(1, Res) == reply
->
Buffer = ?CODEC:new(),
case thrift_processor_codec:write_function_result(Buffer, ?CODEC, Service, Function, Res, SeqId) of
{ok, Buffer1} ->
Response = ?CODEC:close(Buffer1),
reply(200, cowboy_req:set_resp_body(Response, Req), WoodyState);
{error, Reason} ->
handle_encode_error(Reason, Req, WoodyState)
end;
handle_result(Res = {exception, TypeName, Exception}, Service, Function, SeqId, Req, WoodyState) ->
Buffer = ?CODEC:new(),
case thrift_processor_codec:write_function_result(Buffer, ?CODEC, Service, Function, Res, SeqId) of
{ok, Buffer1} ->
ExceptionName = get_exception_name(TypeName, Exception),
Response = ?CODEC:close(Buffer1),
handle_error({business, {ExceptionName, Response}}, Req, WoodyState);
{error, Reason} ->
handle_encode_error(Reason, Req, WoodyState)
end;
handle_result({error, Error}, _Service, _Function, _SeqId, Req, WoodyState) ->
handle_error(Error, Req, WoodyState).
get_exception_name({{struct, exception, {_Mod, Name}}, _}, _) ->
genlib:to_binary(Name);
get_exception_name(_TypeName, Exception) ->
genlib:to_binary(element(1, Exception)).
-spec handle_encode_error(_Reason, cowboy_req:req(), woody_state:st()) ->
cowboy_req:req().
handle_encode_error(Reason, Req, WoodyState) ->
_ = handle_event(
?EV_INTERNAL_ERROR,
WoodyState,
#{
error => <<"thrift protocol write failed">>,
reason => woody_error:format_details(Reason)
}
),
Error = {internal, result_unexpected, format_unexpected_error(error, Reason, [])},
handle_error({system, Error}, Req, WoodyState).
add_ev_meta(WoodyState, Service = {_, ServiceName}, ReplyType, Function, Args) ->
woody_state:add_ev_meta(#{
service => ServiceName,
service_schema => Service,
function => Function,
args => Args,
type => get_rpc_reply_type(ReplyType),
deadline => woody_context:get_deadline(woody_state:get_context(WoodyState))
}, WoodyState).
get_rpc_reply_type(oneway) -> cast;
get_rpc_reply_type(call) -> call.
format_unexpected_error(Class, Reason, Stack) ->
woody_util:to_binary(
[Class, ":", woody_error:format_details(Reason), " ", genlib_format:format_stacktrace(Stack)]
).
-spec handle_error(Error, cowboy_req:req(), woody_state:st()) -> cowboy_req:req() when
Error :: woody_error:error() | woody_server_thrift_handler:client_error().
handle_error({business, {ExceptName, Except}}, Req, WoodyState) ->
reply(200, set_error_headers(<<"Business Error">>, ExceptName, cowboy_req:set_resp_body(Except, Req)), WoodyState);
handle_error({client, Error}, Req, WoodyState) ->
reply(400, set_error_headers(<<"Result Unexpected">>, Error, Req), WoodyState);
handle_error({system, {internal, result_unexpected, Details}}, Req, WoodyState) ->
reply(500, set_error_headers(<<"Result Unexpected">>, Details, Req), WoodyState);
handle_error({system, {internal, resource_unavailable, Details}}, Req, WoodyState) ->
reply(503, set_error_headers(<<"Resource Unavailable">>, Details, Req), WoodyState);
handle_error({system, {internal, result_unknown, Details}}, Req, WoodyState) ->
reply(504, set_error_headers(<<"Result Unknown">>, Details, Req), WoodyState);
handle_error({system, {external, result_unexpected, Details}}, Req, WoodyState) ->
reply(502, set_error_headers(<<"Result Unexpected">>, Details, Req), WoodyState);
handle_error({system, {external, resource_unavailable, Details}}, Req, WoodyState) ->
reply(502, set_error_headers(<<"Resource Unavailable">>, Details, Req), WoodyState);
handle_error({system, {external, result_unknown, Details}}, Req, WoodyState) ->
reply(502, set_error_headers(<<"Result Unknown">>, Details, Req), WoodyState).
-spec set_error_headers(woody:http_header_val(), woody:http_header_val(), cowboy_req:req()) ->
cowboy_req:req().
set_error_headers(Class, Reason, Req) ->
Headers = #{
?HEADER_E_CLASS => Class,
?HEADER_E_REASON => Reason
},
cowboy_req:set_resp_headers(Headers, Req).
-spec reply(woody:http_code(), cowboy_req:req(), woody_state:st()) ->
cowboy_req:req().
reply(200, Req, WoodyState) ->
do_reply(200, cowboy_req:set_resp_header(<<"content-type">>, ?CONTENT_TYPE_THRIFT, Req), WoodyState);
reply(Code, Req, WoodyState) ->
do_reply(Code, Req, WoodyState).
do_reply(Code, Req, WoodyState) ->
_ = handle_event(?EV_SERVER_SEND, WoodyState, #{code => Code, status => reply_status(Code)}),
cowboy_req:reply(Code, Req).
reply_status(200) -> ok;
reply_status(_) -> error.
handle_event(Event, WoodyState, ExtraMeta) ->
woody_event_handler:handle_event(Event, WoodyState, ExtraMeta).
update_woody_state(State, WoodyState, Req) ->
ok = woody_monitor_h:put_woody_state(WoodyState, Req),
State#{woody_state => WoodyState}.

View File

@ -1,8 +1,12 @@
-module(woody_trace_h).
-behaviour(cowboy_stream).
-include("woody_defs.hrl").
-dialyzer(no_undefined_callbacks).
-export([env/1]).
-behaviour(cowboy_stream).
-export([init/3]).
-export([data/4]).
-export([info/3]).
@ -11,34 +15,82 @@
-type state() :: #{
req := cowboy_req:req(),
ev_handler := woody:ev_handler(),
opts := woody:ev_handler(),
next := any()
}.
-define(TRACER, trace_http_server).
-type options() :: #{
event_handler := woody:ev_handlers()
}.
-spec env(options()) ->
cowboy_middleware:env().
env(Opts = #{}) ->
EvHandler = maps:get(event_handler, Opts),
#{?MODULE => EvHandler}.
extract_trace_options(#{env := Env}) ->
maps:get(?MODULE, Env).
%% private functions
trace_request(Req, EvHandler, ReadBodyOpts) ->
woody_server_thrift_http_handler:trace_req(genlib_app:env(woody, ?TRACER), Req, EvHandler, ReadBodyOpts).
trace_request(Req, EvHandler) ->
trace_req(genlib_app:env(woody, ?TRACER), Req, EvHandler).
trace_response(Req, {response, Code, Headers, Body}, EvHandler) ->
woody_server_thrift_http_handler:trace_resp(genlib_app:env(woody, ?TRACER), Req, Code, Headers, Body, EvHandler).
trace_resp(genlib_app:env(woody, ?TRACER), Req, Code, Headers, Body, EvHandler).
% We can't go without Env AND event handler, so we want to fail, reading opts are optional tho
extract_trace_options(Opts) ->
Env = maps:get(env, Opts),
{maps:get(event_handler, Env), maps:get(read_body_opts, Env, #{})}.
-spec trace_req(true, cowboy_req:req(), woody:ev_handlers()) ->
cowboy_req:req().
trace_req(true, Req, EvHandler) ->
Url = unicode:characters_to_binary(cowboy_req:uri(Req)),
Headers = cowboy_req:headers(Req),
% TODO
% No body here since with Cowboy 2 we can consume it only once.
% Ideally we would need to embed tracing directly into handler itself.
Meta = #{
role => server,
event => <<"http request received">>,
url => Url,
headers => Headers
},
_ = woody_event_handler:handle_event(EvHandler, ?EV_TRACE, undefined, Meta),
Req;
trace_req(_, Req, _) ->
Req.
-spec trace_resp(
true,
cowboy_req:req(),
woody:http_code(),
woody:http_headers(),
woody:http_body(),
woody:ev_handlers()
) ->
cowboy_req:req().
trace_resp(true, Req, Code, Headers, Body, EvHandler) ->
_ = woody_event_handler:handle_event(EvHandler, ?EV_TRACE, undefined, #{
role => server,
event => <<"http response send">>,
code => Code,
headers => Headers,
body => Body
}),
Req;
trace_resp(_, Req, _, _, _, _) ->
Req.
%% callbacks
-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
-> {cowboy_stream:commands(), state()}.
init(StreamID, Req, Opts) ->
{EvHandler, ReadBodyOpts} = extract_trace_options(Opts),
_ = trace_request(Req, EvHandler, ReadBodyOpts),
TraceOpts = extract_trace_options(Opts),
_ = trace_request(Req, TraceOpts),
{Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts),
{Commands0, #{next => Next, req => Req, ev_handler => EvHandler}}.
{Commands0, #{next => Next, req => Req, opts => TraceOpts}}.
-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
-> {cowboy_stream:commands(), State} when State::state().
@ -48,8 +100,8 @@ data(StreamID, IsFin, Data, #{next := Next0} = State) ->
-spec info(cowboy_stream:streamid(), any(), State)
-> {cowboy_stream:commands(), State} when State::state().
info(StreamID, {response, _, _, _} = Info, #{next := Next0, req := Req, ev_handler := EvHandler} = State) ->
_ = trace_response(Req, Info, EvHandler),
info(StreamID, {response, _, _, _} = Info, #{next := Next0, req := Req, opts := TraceOpts} = State) ->
_ = trace_response(Req, Info, TraceOpts),
{Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0),
{Commands0, State#{next => Next}};
info(StreamID, Info, #{next := Next0} = State) ->
@ -65,7 +117,7 @@ terminate(StreamID, Reason, #{next := Next}) ->
cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
when Resp::cowboy_stream:resp_command().
early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
{EvHandler, ReadBodyOpts} = extract_trace_options(Opts),
_ = trace_request(PartialReq, EvHandler, ReadBodyOpts),
_ = trace_response(PartialReq, Resp, EvHandler),
TraceOpts = extract_trace_options(Opts),
_ = trace_request(PartialReq, TraceOpts),
_ = trace_response(PartialReq, Resp, TraceOpts),
cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).

View File

@ -6,6 +6,7 @@
-export([get_protocol_handler/2]).
-export([get_mod_opts/1]).
-export([to_binary/1]).
-export([get_rpc_type/2]).
-export([get_rpc_reply_type/1]).
-define(DEFAULT_HANDLER_OPTS, undefined).
@ -14,13 +15,15 @@
%% Internal API
%%
-spec get_protocol_handler(woody:role(), map()) ->
woody_client_thrift | woody_server_thrift_http_handler | no_return().
module() | no_return().
get_protocol_handler(_Role, #{protocol_handler_override := Module}) when is_atom(Module) ->
Module;
get_protocol_handler(Role, Opts) ->
Protocol = genlib_map:get(protocol, Opts, thrift),
Transport = genlib_map:get(transport, Opts, http),
case {Role, Protocol, Transport} of
{client, thrift, http} -> woody_client_thrift;
{server, thrift, http} -> woody_server_thrift_http_handler;
{client, thrift, http} -> woody_client_thrift_v2;
{server, thrift, http} -> woody_server_thrift_v2;
_ -> error(badarg, [Role, Opts])
end.
@ -44,6 +47,11 @@ to_binary([Part | T], Reason) ->
BinPart = genlib:to_binary(Part),
to_binary(T, <<Reason/binary, BinPart/binary>>).
-spec get_rpc_type(woody:service(), woody:func()) ->
woody:rpc_type().
get_rpc_type({Module, Service}, Function) ->
get_rpc_reply_type(Module:function_info(Service, Function, reply_type)).
-spec get_rpc_reply_type(_ThriftReplyType) ->
woody:rpc_type().
get_rpc_reply_type(oneway_void) -> cast;

View File

@ -199,7 +199,7 @@ handle_event(Event, RpcId, Meta, _) ->
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) ->
{ok, woody:result()}.
handle_function(get_weapon, [Name, _Data], Context, _Opts) ->
handle_function(get_weapon, {Name, _Data}, Context, _Opts) ->
_ = assert_common_name([<<"Valid Test Client">>], Context),
{ok, #'Weapon'{name = Name, slot_pos = 0}}.
@ -259,7 +259,7 @@ get_weapon(Id, Gun, SSLOptions) ->
]
}
},
woody_client:call({Service, get_weapon, [Gun, <<>>]}, Options, Context).
woody_client:call({Service, get_weapon, {Gun, <<>>}}, Options, Context).
get_service_endpoint('Weapons') ->
{

View File

@ -239,24 +239,17 @@
%% tests descriptions
%%
all() ->
[{group, G} || G <- maps:keys(cross_test_groups())] ++
[
{group, client_server},
ids_monotonic_incr_test,
{group, contexts},
{group, deadlines},
{group, woody_resolver}
].
groups() ->
SpecTests = [
context_add_put_get_meta_ok_test,
context_get_meta_by_key_ok_test,
context_get_empty_meta_ok_test,
context_get_empty_meta_by_key_ok_test,
context_given_rpc_id_test,
context_given_id_test,
context_generated_rpc_id_test,
ids_monotonic_incr_test,
deadline_reached_test,
deadline_to_from_timeout_test,
deadline_to_from_binary_test,
call_ok_test,
call_resolver_nxdomain,
call3_ok_test,
@ -290,8 +283,23 @@ groups() ->
find_multiple_pools_test,
calls_with_cache
],
[{G, [], SpecTests} || G <- maps:keys(cross_test_groups())] ++
[
{client_server, [], SpecTests},
{contexts, [], [
context_add_put_get_meta_ok_test,
context_get_meta_by_key_ok_test,
context_get_empty_meta_ok_test,
context_get_empty_meta_by_key_ok_test,
context_given_rpc_id_test,
context_given_id_test,
context_generated_rpc_id_test
]},
{deadlines, [], [
deadline_reached_test,
deadline_to_from_timeout_test,
deadline_to_from_binary_test
]},
{woody_resolver, [], [
woody_resolver_inet,
woody_resolver_inet6,
@ -299,6 +307,22 @@ groups() ->
]}
].
cross_test_groups() ->
#{
'client_v1.server_v1' => {
#{protocol_handler_override => woody_client_thrift},
#{protocol_handler_override => woody_server_thrift_http_handler}
},
'client_v1.server_v2' => {
#{protocol_handler_override => woody_client_thrift},
#{}
},
'client_v2.server_v1' => {
#{},
#{protocol_handler_override => woody_server_thrift_http_handler}
}
}.
%%
%% starting/stopping
%%
@ -351,28 +375,29 @@ init_per_testcase(find_multiple_pools_test, C) ->
{ok, Sup} = start_tc_sup(),
Pool1 = {swords, 15000, 100},
Pool2 = {shields, undefined, 50},
ok = start_woody_server_with_pools(woody_ct, Sup, ['Weapons', 'Powerups'], [Pool1, Pool2]),
ok = start_woody_server_with_pools(woody_ct, Sup, ['Weapons', 'Powerups'], [Pool1, Pool2], C),
[{sup, Sup} | C];
init_per_testcase(calls_with_cache, C) ->
{ok, Sup} = start_tc_sup(),
{ok, _} = start_caching_client(caching_client_ct, Sup),
{ok, _} = start_woody_server(woody_ct, Sup, ['Weapons', 'Powerups']),
{ok, _} = start_woody_server(woody_ct, Sup, ['Weapons', 'Powerups'], C),
[{sup, Sup} | C];
init_per_testcase(server_handled_client_timeout_test, C) ->
{ok, Sup} = start_tc_sup(),
{ok, _} = supervisor:start_child(Sup, server_timeout_event_handler:child_spec()),
{ok, _} = start_woody_server(woody_ct, Sup, ['Weapons', 'Powerups'], server_timeout_event_handler),
{ok, _} = start_woody_server(woody_ct, Sup, ['Weapons', 'Powerups'], server_timeout_event_handler, C),
[{sup, Sup} | C];
init_per_testcase(_, C) ->
{ok, Sup} = start_tc_sup(),
{ok, _} = start_woody_server(woody_ct, Sup, ['Weapons', 'Powerups']),
{ok, _} = start_woody_server(woody_ct, Sup, ['Weapons', 'Powerups'], C),
[{sup, Sup} | C].
init_per_group(woody_resolver, Config) ->
[{env_inet6, inet_db:res_option(inet6)} | Config];
init_per_group(_Name, Config) ->
Config.
init_per_group(Name, Config) ->
{ClientOpts, ServerOpts} = maps:get(Name, cross_test_groups(), {#{}, #{}}),
[{client_opts, ClientOpts}, {server_opts, ServerOpts} | Config].
end_per_group(_Name, _Config) ->
ok.
@ -389,25 +414,33 @@ start_error_server(TC, Sup) ->
),
supervisor:start_child(Sup, Server).
start_woody_server(Id, Sup, Services) ->
start_woody_server(Id, Sup, Services, ?MODULE).
start_woody_server(Id, Sup, Services, C) ->
start_woody_server(Id, Sup, Services, ?MODULE, C).
start_woody_server(Id, Sup, Services, EventHandler) ->
Server = woody_server:child_spec(Id, #{
start_woody_server(Id, Sup, Services, EventHandler, C) ->
Opts = maps:merge(
proplists:get_value(server_opts, C, #{}),
#{
handlers => [get_handler(S) || S <- Services],
event_handler => EventHandler,
ip => ?SERVER_IP,
port => ?SERVER_PORT
}),
}
),
Server = woody_server:child_spec(Id, Opts),
supervisor:start_child(Sup, Server).
start_woody_server_with_pools(Id, Sup, Services, Params) ->
Server = woody_server:child_spec(Id, #{
start_woody_server_with_pools(Id, Sup, Services, Params, C) ->
Opts = maps:merge(
proplists:get_value(server_opts, C, #{}),
#{
handlers => [get_handler(S) || S <- Services],
event_handler => ?MODULE,
ip => ?SERVER_IP,
port => ?SERVER_PORT
}),
}
),
Server = woody_server:child_spec(Id, Opts),
{ok, WoodyServer} = supervisor:start_child(Sup, Server),
Specs = [woody_client:child_spec(pool_opts(Pool)) || Pool <- Params],
@ -586,144 +619,144 @@ deadline_to_from_binary_test(_) ->
ok
end.
call_ok_test(_) ->
call_ok_test(C) ->
Gun = <<"Enforcer">>,
gun_test_basic(<<"call_ok">>, Gun, {ok, genlib_map:get(Gun, ?WEAPONS)}, true).
gun_test_basic(<<"call_ok">>, Gun, {ok, genlib_map:get(Gun, ?WEAPONS)}, true, C).
call_resolver_nxdomain(_) ->
call_resolver_nxdomain(C) ->
Context = make_context(<<"nxdomain">>),
?assertError(
{woody_error, {internal, resource_unavailable, <<"{resolve_failed,nxdomain}">>}},
call(Context, 'The Void', get_weapon, [<<"Enforcer">>, self_to_bin()])
call(Context, 'The Void', get_weapon, {<<"Enforcer">>, self_to_bin()}, C)
).
call3_ok_test(_) ->
call3_ok_test(C) ->
{Url, Service} = get_service_endpoint('Weapons'),
Gun = <<"Enforcer">>,
Request = {Service, get_weapon, [Gun, self_to_bin()]},
Opts = #{url => Url, event_handler => ?MODULE},
Request = {Service, get_weapon, {Gun, self_to_bin()}},
Opts = mk_client_opts(#{url => Url}, C),
Expect = {ok, genlib_map:get(Gun, ?WEAPONS)},
Expect = woody_client:call(Request, Opts).
call3_ok_default_ev_handler_test(_) ->
call3_ok_default_ev_handler_test(C) ->
{Url, Service} = get_service_endpoint('Weapons'),
Gun = <<"Enforcer">>,
Request = {Service, get_weapon, [Gun, self_to_bin()]},
Opts = #{url => Url, event_handler => {woody_event_handler_default, #{}}},
Request = {Service, get_weapon, {Gun, self_to_bin()}},
Opts = mk_client_opts(#{url => Url, event_handler => {woody_event_handler_default, #{}}}, C),
Expect = {ok, genlib_map:get(Gun, ?WEAPONS)},
Expect = woody_client:call(Request, Opts).
call_business_error_test(_) ->
call_business_error_test(C) ->
Gun = <<"Bio Rifle">>,
gun_test_basic(<<"call_business_error">>, Gun,
{exception, ?WEAPON_FAILURE("out of ammo")}, true).
{exception, ?WEAPON_FAILURE("out of ammo")}, true, C).
call_throw_unexpected_test(_) ->
call_throw_unexpected_test(C) ->
Id = <<"call_throw_unexpected">>,
Current = genlib_map:get(<<"Rocket Launcher">>, ?WEAPONS),
Context = make_context(Id),
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Weapons', switch_weapon, [Current, next, 1, self_to_bin()])
call(Context, 'Weapons', switch_weapon, {Current, next, 1, self_to_bin()}, C)
).
call_system_external_error_test(_) ->
call_system_external_error_test(C) ->
Id = <<"call_system_external_error">>,
Gun = <<"The Ultimate Super Mega Destroyer">>,
Context = make_context(Id),
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Weapons', get_weapon, [Gun, self_to_bin()])
call(Context, 'Weapons', get_weapon, {Gun, self_to_bin()}, C)
).
call_client_error_test(_) ->
call_client_error_test(C) ->
Gun = 'Wrong Type of Mega Destroyer',
Context = make_context(<<"call_client_error">>),
?assertError(
{woody_error, {internal, result_unexpected, <<"client thrift error: ", _/binary>>}},
call(Context, 'Weapons', get_weapon, [Gun, self_to_bin()])
call(Context, 'Weapons', get_weapon, {Gun, self_to_bin()}, C)
).
call_server_internal_error_test(_) ->
call_server_internal_error_test(C) ->
Armor = <<"Helmet">>,
Context = make_context(<<"call_server_internal_error">>),
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Powerups', get_powerup, [Armor, self_to_bin()])
call(Context, 'Powerups', get_powerup, {Armor, self_to_bin()}, C)
),
{ok, _} = receive_msg(Armor, Context).
call_oneway_void_test(_) ->
call_oneway_void_test(C) ->
Armor = <<"Helmet">>,
Context = make_context(<<"call_oneway_void">>),
{ok, ok} = call(Context, 'Powerups', like_powerup, [Armor, self_to_bin()]),
{ok, ok} = call(Context, 'Powerups', like_powerup, {Armor, self_to_bin()}, C),
{ok, _} = receive_msg(Armor, Context).
call_sequence_with_context_meta_test(_) ->
call_sequence_with_context_meta_test(C) ->
Gun = <<"Enforcer">>,
Current = genlib_map:get(Gun, ?WEAPONS),
Context = woody_context:new(
<<"call_seq_with_context_meta">>,
#{genlib:to_binary(Current#'Weapon'.slot_pos) => Gun}),
Expect = {ok, genlib_map:get(<<"Ripper">>, ?WEAPONS)},
Expect = call(Context, 'Weapons', switch_weapon, [Current, next, 1, self_to_bin()]).
Expect = call(Context, 'Weapons', switch_weapon, {Current, next, 1, self_to_bin()}, C).
call_pass_thru_ok_test(_) ->
call_pass_thru_ok_test(C) ->
Armor = <<"AntiGrav Boots">>,
Context = make_context(<<"call_pass_thru_ok">>),
Expect = {ok, genlib_map:get(Armor, ?POWERUPS)},
Expect = call(Context, 'Powerups', proxy_get_powerup, [Armor, self_to_bin()]),
Expect = call(Context, 'Powerups', proxy_get_powerup, {Armor, self_to_bin()}, C),
{ok, _} = receive_msg(Armor, Context).
call_pass_thru_except_test(_) ->
call_pass_thru_except_test(C) ->
Armor = <<"Shield Belt">>,
Id = <<"call_pass_thru_except">>,
RpcId = woody_context:new_rpc_id(?ROOT_REQ_PARENT_ID, Id, Id),
Context = woody_context:new(RpcId),
try call(Context, 'Powerups', proxy_get_powerup, [Armor, self_to_bin()])
try call(Context, 'Powerups', proxy_get_powerup, {Armor, self_to_bin()}, C)
catch
error:{woody_error, {external, result_unexpected, _}} ->
ok
end,
{ok, _} = receive_msg(Armor, Context).
call_pass_thru_bad_result_test(_) ->
call_pass_thru_bad_result_test(C) ->
Armor = <<"AntiGrav Boots">>,
Context = make_context(<<"call_pass_thru_bad_result">>),
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Powerups', bad_proxy_get_powerup, [Armor, self_to_bin()])
call(Context, 'Powerups', bad_proxy_get_powerup, {Armor, self_to_bin()}, C)
),
{ok, _} = receive_msg(Armor, Context).
call_pass_thru_bad_except_test(_) ->
call_pass_thru_bad_except_test(C) ->
Armor = <<"Shield Belt">>,
Context = make_context(<<"call_pass_thru_bad_except">>),
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Powerups', bad_proxy_get_powerup, [Armor, self_to_bin()])
call(Context, 'Powerups', bad_proxy_get_powerup, {Armor, self_to_bin()}, C)
),
{ok, _} = receive_msg(Armor, Context).
call_pass_thru_result_unexpected_test(_) ->
call_pass_thru_result_unexpected_test(C) ->
call_pass_thru_error(<<"call_pass_thru_result_unexpected">>, <<"Helmet">>,
error, result_unexpected).
error, result_unexpected, C).
call_pass_thru_resource_unavail_test(_) ->
call_pass_thru_resource_unavail_test(C) ->
call_pass_thru_error(<<"call_pass_thru_resource_unavail">>, <<"Damage Amplifier">>,
error, resource_unavailable).
error, resource_unavailable, C).
call_pass_thru_result_unknown_test(_) ->
call_pass_thru_result_unknown_test(C) ->
call_pass_thru_error(<<"call_pass_thru_result_unknown">>, <<"Invisibility">>,
error, result_unknown).
error, result_unknown, C).
call_pass_thru_error(Id, Powerup, ExceptClass, ErrClass) ->
call_pass_thru_error(Id, Powerup, ExceptClass, ErrClass, C) ->
RpcId = woody_context:new_rpc_id(?ROOT_REQ_PARENT_ID, Id, Id),
Context = woody_context:new(RpcId),
?assertException(
ExceptClass,
{woody_error, {external, ErrClass, _}},
call(Context, 'Powerups', proxy_get_powerup, [Powerup, self_to_bin()])
call(Context, 'Powerups', proxy_get_powerup, {Powerup, self_to_bin()}, C)
),
{ok, _} = receive_msg(Powerup, Context).
@ -749,7 +782,7 @@ call_fail_w_no_headers(Id, Class, Code) ->
BinCode = integer_to_binary(Code),
?assertError(
{woody_error, {external, Class, <<"got response with http code ", BinCode:3/binary, _/binary>>}},
woody_client:call({Service, get_weapon, [Gun, self_to_bin()]},
woody_client:call({Service, get_weapon, {Gun, self_to_bin()}},
#{url => Url, event_handler => ?MODULE}, Context)
).
@ -775,23 +808,23 @@ make_thrift_multiplexed_client(Id, ServiceName, {Url, Service}) ->
{ok, Client} = thrift_client:new(Protocol1, Service),
Client.
call_deadline_ok_test(_) ->
call_deadline_ok_test(C) ->
Id = <<"call_deadline_timeout">>,
{Url, Service} = get_service_endpoint('Weapons'),
Gun = <<"Enforcer">>,
Request = {Service, get_weapon, [Gun, self_to_bin()]},
Opts = #{url => Url, event_handler => ?MODULE},
Request = {Service, get_weapon, {Gun, self_to_bin()}},
Opts = mk_client_opts(#{url => Url}, C),
Deadline = woody_deadline:from_timeout(3000),
Context = woody_context:new(Id, #{<<"sleep">> => <<"100">>}, Deadline),
Expect = {ok, genlib_map:get(Gun, ?WEAPONS)},
Expect = woody_client:call(Request, Opts, Context).
call_deadline_reached_on_client_test(_) ->
call_deadline_reached_on_client_test(C) ->
Id = <<"call_deadline_reached_on_client">>,
{Url, Service} = get_service_endpoint('Weapons'),
Gun = <<"Enforcer">>,
Request = {Service, get_weapon, [Gun, self_to_bin()]},
Opts = #{url => Url, event_handler => ?MODULE},
Request = {Service, get_weapon, {Gun, self_to_bin()}},
Opts = mk_client_opts(#{url => Url}, C),
Deadline = woody_deadline:from_timeout(0),
Context = woody_context:new(Id, #{<<"sleep">> => <<"1000">>}, Deadline),
?assertError(
@ -799,11 +832,11 @@ call_deadline_reached_on_client_test(_) ->
woody_client:call(Request, Opts, Context)
).
server_handled_client_timeout_test(_) ->
server_handled_client_timeout_test(C) ->
Id = <<"server_handled_client_timeout">>,
{Url, Service} = get_service_endpoint('Weapons'),
Request = {Service, get_stuck_looping_weapons, []},
Opts = #{url => Url, event_handler => ?MODULE},
Request = {Service, get_stuck_looping_weapons, {}},
Opts = mk_client_opts(#{url => Url}, C),
Deadline = woody_deadline:from_timeout(250),
Context = woody_context:new(Id, #{}, Deadline),
try
@ -816,12 +849,12 @@ server_handled_client_timeout_test(_) ->
ok
end.
call_deadline_timeout_test(_) ->
call_deadline_timeout_test(C) ->
Id = <<"call_deadline_timeout">>,
{Url, Service} = get_service_endpoint('Weapons'),
Gun = <<"Enforcer">>,
Request = {Service, get_weapon, [Gun, self_to_bin()]},
Opts = #{url => Url, event_handler => ?MODULE},
Request = {Service, get_weapon, {Gun, self_to_bin()}},
Opts = mk_client_opts(#{url => Url}, C),
Deadline = woody_deadline:from_timeout(500),
Context = woody_context:new(Id, #{<<"sleep">> => <<"3000">>}, Deadline),
?assertError(
@ -883,8 +916,8 @@ try_bad_handler_spec_test(_) ->
calls_with_cache(_) ->
Id = <<"call_with_cache">>,
{_, Service} = get_service_endpoint('Weapons'),
Request = {Service, get_weapon, [<<"Enforcer">>, self_to_bin()]},
InvalidRequest = {Service, get_weapon, [<<"Bio Rifle">>, self_to_bin()]},
Request = {Service, get_weapon, {<<"Enforcer">>, self_to_bin()}},
InvalidRequest = {Service, get_weapon, {<<"Bio Rifle">>, self_to_bin()}},
Opts = woody_caching_client_options(),
Context = woody_context:new(Id),
@ -963,13 +996,13 @@ init(_) ->
%%
%% Weapons
handle_function(switch_weapon, [CurrentWeapon, Direction, Shift, To], Context, #{meta_test_id := AnnotTestId}) ->
handle_function(switch_weapon, {CurrentWeapon, Direction, Shift, To}, Context, #{meta_test_id := AnnotTestId}) ->
ok = send_msg(To, {woody_context:get_rpc_id(parent_id, Context), CurrentWeapon}),
CheckAnnot = is_meta_check_required(AnnotTestId, woody_context:get_rpc_id(trace_id, Context)),
ok = check_meta(CheckAnnot, Context, CurrentWeapon),
switch_weapon(CurrentWeapon, Direction, Shift, Context, CheckAnnot);
switch_weapon(CurrentWeapon, Direction, Shift, Context, CheckAnnot, []);
handle_function(get_weapon, [Name, To], Context, _Opts) ->
handle_function(get_weapon, {Name, To}, Context, _Opts) ->
ok = handle_sleep(Context),
ok = send_msg(To, {woody_context:get_rpc_id(parent_id, Context), Name}),
case genlib_map:get(Name, ?WEAPONS) of
@ -980,15 +1013,19 @@ handle_function(get_weapon, [Name, To], Context, _Opts) ->
end;
%% Powerups
handle_function(get_powerup, [Name, To], Context, _Opts) ->
handle_function(get_powerup, {Name, To}, Context, _Opts) ->
ok = send_msg(To, {woody_context:get_rpc_id(parent_id, Context), Name}),
{ok, return_powerup(Name)};
handle_function(ProxyGetPowerup, [Name, To], Context, _Opts) when
handle_function(ProxyGetPowerup, {Name, To}, Context, _Opts) when
ProxyGetPowerup =:= proxy_get_powerup orelse
ProxyGetPowerup =:= bad_proxy_get_powerup
->
try call(Context, 'Powerups', get_powerup, [Name, self_to_bin()])
% NOTE
% Client may return `{exception, _}` tuple with some business level exception
% here, yet handler expects us to `throw/1` them. This is expected here it
% seems though.
try call(Context, 'Powerups', get_powerup, {Name, self_to_bin()}, [])
catch
Class:Reason:Stacktrace ->
erlang:raise(Class, Reason, Stacktrace)
@ -997,7 +1034,7 @@ handle_function(ProxyGetPowerup, [Name, To], Context, _Opts) when
ok = send_msg(To, {woody_context:get_rpc_id(parent_id, Context), Name})
end;
handle_function(like_powerup, [Name, To], Context, _Opts) ->
handle_function(like_powerup, {Name, To}, Context, _Opts) ->
ok = send_msg(To, {woody_context:get_rpc_id(parent_id, Context), Name}),
{ok, ok};
@ -1063,7 +1100,7 @@ log_event(Event, RpcId, Meta) ->
%%
init(Req, Code) ->
ct:pal("Cowboy fail server received request. Replying: ~p", [Code]),
{stop, cowboy_req:reply(Code, Req), Code}.
{ok, cowboy_req:reply(Code, Req), Code}.
terminate(_, _, _) ->
ok.
@ -1071,10 +1108,10 @@ terminate(_, _, _) ->
%%
%% internal functions
%%
gun_test_basic(Id, Gun, Expect, WithMsg) ->
gun_test_basic(Id, Gun, Expect, WithMsg, C) ->
Context = make_context(Id),
{Class, Reason} = get_except(Expect),
try call(Context, 'Weapons', get_weapon, [Gun, self_to_bin()]) of
try call(Context, 'Weapons', get_weapon, {Gun, self_to_bin()}, C) of
Expect -> ok
catch
Class:Reason -> ok
@ -1091,9 +1128,9 @@ get_except(Except = {_Class, _Reason}) ->
make_context(ReqId) ->
woody_context:new(ReqId).
call(Context, ServiceName, Function, Args) ->
call(Context, ServiceName, Function, Args, C) ->
{Url, Service} = get_service_endpoint(ServiceName),
woody_client:call({Service, Function, Args}, #{url => Url, event_handler => ?MODULE}, Context).
woody_client:call({Service, Function, Args}, mk_client_opts(#{url => Url}, C), Context).
get_service_endpoint('Weapons') ->
{
@ -1116,13 +1153,13 @@ check_msg(true, Msg, Context) ->
check_msg(false, _, _) ->
ok.
switch_weapon(CurrentWeapon, Direction, Shift, Context, CheckAnnot) ->
switch_weapon(CurrentWeapon, Direction, Shift, Context, CheckAnnot, C) ->
{NextWName, NextWPos} = next_weapon(CurrentWeapon, Direction, Shift),
ContextAnnot = annotate_weapon(CheckAnnot, Context, NextWName, NextWPos),
case call(ContextAnnot, 'Weapons', get_weapon, [NextWName, self_to_bin()]) of
case call(ContextAnnot, 'Weapons', get_weapon, {NextWName, self_to_bin()}, C) of
{exception, #'WeaponFailure'{code = <<"weapon_error">>, reason = <<"out of ammo">>}}
->
switch_weapon(CurrentWeapon, Direction, Shift + 1, Context, CheckAnnot);
switch_weapon(CurrentWeapon, Direction, Shift + 1, Context, CheckAnnot, C);
Ok ->
Ok
end.
@ -1155,6 +1192,13 @@ return_powerup(P = #'Powerup'{}) ->
return_powerup(P = ?BAD_POWERUP_REPLY) ->
P.
mk_client_opts(TestCaseOpts, C) ->
GroupOpts = proplists:get_value(client_opts, C, #{}),
lists:foldl(fun maps:merge/2, GroupOpts, [
#{event_handler => ?MODULE},
TestCaseOpts
]).
self_to_bin() ->
genlib:to_binary(pid_to_list(self())).

View File

@ -100,7 +100,7 @@ respects_max_connections(C) ->
{ok, ServerPid} = start_woody_server(Handler, TransportOpts, ProtocolOpts, ReadBodyOpts, C),
Results = genlib_pmap:map(
fun (_) ->
woody_client:call({Service, 'get_weapon', [<<"BFG">>, <<>>]}, Client)
woody_client:call({Service, 'get_weapon', {<<"BFG">>, <<>>}}, Client)
end,
lists:seq(1, MaxConns * 10)
),
@ -176,13 +176,13 @@ stop_woody_server(Pid) ->
true = exit(Pid, shutdown),
ok.
handle_function(get_weapon, [Name, _], _Context, {respects_max_connections, Table}) ->
handle_function(get_weapon, {Name, _}, _Context, {respects_max_connections, Table}) ->
Slot = ets:update_counter(Table, slot, 1),
ok = timer:sleep(rand:uniform(10)),
_ = ets:update_counter(Table, slot, -1),
{ok, #'Weapon'{name = Name, slot_pos = Slot}};
handle_function(get_powerup, [Name, _], _Context, _) ->
handle_function(get_powerup, {Name, _}, _Context, _) ->
ok = timer:sleep(2000),
{ok, #'Powerup'{name = Name}}.
@ -191,7 +191,7 @@ handle_event(Event, RpcId, Meta, Opts) ->
ct:pal("~p " ++ Format, [Opts] ++ Msg).
get_powerup(Client, Name, Arg) ->
woody_client:call({{woody_test_thrift, 'Powerups'}, 'get_powerup', [Name, Arg]}, Client).
woody_client:call({{woody_test_thrift, 'Powerups'}, 'get_powerup', {Name, Arg}}, Client).
%%