Implement binary-first event formatter (#122)

* Add compex term formatting microbenchmark
* Format FP numbers more neatly
* Provide fast-path verbatim term formatter
* Add memory pressure benchmark runner
* Switch to rebar3_bench 0.2.1 package
* Employ assert macros throughout main ct suite
This commit is contained in:
Andrew Mayorov 2019-12-31 12:52:56 +03:00 committed by GitHub
parent f74bb6854c
commit e615c85d70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 887 additions and 568 deletions

2
Jenkinsfile vendored
View File

@ -33,7 +33,7 @@ build('woody_erlang', 'docker-host', finalHook) {
}
runStage('dialyze') {
withGithubPrivkey {
withWsCache("_build/test/rebar3_21.3.8.4_plt") {
withWsCache("_build/test/rebar3_21.3.8.7_plt") {
sh 'make wc_dialyze'
}
}

View File

@ -7,9 +7,9 @@ UTILS_PATH := build_utils
# with handling of the varriable in build_utils is fixed
TEMPLATES_PATH := .
SERVICE_NAME := woody
BUILD_IMAGE_TAG := cd38c35976f3684fe7552533b6175a4c3460e88b
BUILD_IMAGE_TAG := 4536c31941b9c27c134e8daf0fd18848809219c9
CALL_W_CONTAINER := all submodules rebar-update compile xref lint test dialyze clean distclean
CALL_W_CONTAINER := all submodules compile xref lint test bench dialyze clean distclean
.PHONY: $(CALL_W_CONTAINER)
@ -23,10 +23,7 @@ $(SUBTARGETS): %/.git: %
submodules: $(SUBTARGETS)
rebar-update:
$(REBAR) update
compile: submodules rebar-update
compile: submodules
$(REBAR) compile
test: submodules
@ -54,4 +51,8 @@ dialyze:
$(REBAR) as test dialyzer
bench:
$(REBAR) as test bench -n 1000
$(REBAR) as test bench -m bench_woody_event_handler -n 1000
$(REBAR) as test bench -m bench_woody_formatter -n 10
erl -pa _build/test/lib/*/ebin _build/test/lib/woody/test -noshell \
-s benchmark_memory_pressure run \
-s erlang halt

View File

@ -22,11 +22,10 @@
{elvis_style, dont_repeat_yourself, #{min_complexity => 17, ignore => [woody_test_thrift, woody_tests_SUITE]}},
{elvis_style, no_debug_call, #{
ignore => [
elvis,
elvis_utils,
woody_ssl_SUITE,
woody_tests_SUITE,
woody_transport_opts_SUITE
woody_transport_opts_SUITE,
benchmark_memory_pressure
]
}}
]

View File

@ -51,7 +51,7 @@
{profiles, [
{test, [
{plugins, [
{rebar3_bench, "0.2.0"}
{rebar3_bench, "0.2.1"}
]},
{cover_enabled, true},
{provider_hooks, [

View File

@ -1,7 +1,6 @@
-module(woody_event_formatter).
-export([
format_call/4,
format_call/5,
format_reply/5,
format_exception/5,
@ -9,85 +8,68 @@
]).
-define(MAX_PRINTABLE_LIST_LENGTH, 3).
-define(MAX_FLOAT_DECIMALS, 8).
%% Binaries under size below will log as-is.
-define(MAX_BIN_SIZE, 40).
-type limit() :: non_neg_integer() | unlimited.
-type opts():: #{
max_depth => integer(),
max_length => integer(),
max_depth => limit(),
max_length => limit(),
max_pritable_string_length => non_neg_integer()
}.
-export_type([opts/0]).
-spec format_call(atom(), atom(), atom(), term()) ->
woody_event_handler:msg().
format_call(Module, Service, Function, Arguments) ->
ConfigOpts = genlib_app:env(woody, event_formatter_options, #{}),
format_call(Module, Service, Function, Arguments, ConfigOpts).
-spec format_call(atom(), atom(), atom(), term(), opts()) ->
woody_event_handler:msg().
format_call(Module, Service, Function, Arguments, Opts) ->
case Module:function_info(Service, Function, params_type) of
{struct, struct, ArgTypes} ->
Opts1 = normalize_options(Opts),
ServiceName = to_string(Service),
ServiceLength = length(ServiceName),
FunctionName = to_string(Function),
FunctionLength = length(FunctionName),
NewCL = ServiceLength + FunctionLength + 3,
{Result, _} =
format_call_(ArgTypes, Arguments, "", 0, NewCL, Opts1, false),
{"~s:~s(~ts)", [ServiceName, FunctionName, Result]};
_Other ->
{"~s:~s(~0tp)", [Service, Function, Arguments]}
end.
Result0 = <<ServiceName/binary, ":", FunctionName/binary, "(">>,
Result = try Module:function_info(Service, Function, params_type) of
{struct, struct, ArgTypes} ->
ML = maps:get(max_length, Opts1),
MD = maps:get(max_depth, Opts1),
MPSL = maps:get(max_pritable_string_length, Opts1),
Result1 = format_call_(ArgTypes, Arguments, Result0, MD, dec(ML), MPSL, false),
<<Result1/binary, ")">>
catch error:badarg ->
Result1 = format_verbatim(Arguments, Result0, Opts1),
<<Result1/binary, ")">>
end,
{"~ts", [Result]}.
format_call_([], [], Result, _CurDepth, CL, _Opts, _AddDelimiter) ->
{Result, CL};
format_call_(_, ArgumentList, Result, _CurDepth, CL, #{max_length := ML}, AddDelimiter) when ML >= 0, CL > ML ->
HasMoreArguments = AddDelimiter and ArgumentList =/= [],
Delimiter = maybe_add_delimiter(HasMoreArguments),
DelimiterLen = length(Delimiter),
MoreArguments = maybe_add_more_marker(HasMoreArguments),
MoreArgumentsLen = length(MoreArguments),
{[Result | [Delimiter, MoreArguments]], CL + DelimiterLen + MoreArgumentsLen};
format_call_([Type | RestType], [Argument | RestArgs], Acc, CurDepth, CL, Opts, AddDelimiter) ->
Delimiter = maybe_add_delimiter(AddDelimiter),
DelimiterLen = length(Delimiter),
case format_argument(Type, Argument, CurDepth, CL + DelimiterLen, Opts) of
{undefined, NewCL} ->
format_call_([], [], Result, _MD, _ML, _MPSL, _AD) ->
Result;
format_call_(_, ArgumentList, Result0, _MD, ML, _MPSL, AD) when byte_size(Result0) > ML ->
HasMoreArguments = AD and (ArgumentList =/= []),
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) ->
{Result1, AD1} = format_argument(Type, Argument, Result0, MD, ML, MPSL, AD),
format_call_(
RestType,
RestArgs,
Acc,
CurDepth,
NewCL,
Opts,
AddDelimiter
);
{Result, NewCL} ->
format_call_(
RestType,
RestArgs,
[Acc | [Delimiter, Result]],
CurDepth,
NewCL + DelimiterLen,
Opts,
true
)
end.
Result1,
MD,
ML,
MPSL,
AD1
).
format_argument({_Fid, _Required, _Type, _Name, undefined}, undefined, _CurDepth, CL, _Opts) ->
{undefined, CL};
format_argument({Fid, Required, Type, Name, Default}, undefined, CurDepth, CL, Opts) ->
format_argument({Fid, Required, Type, Name, Default}, Default, CurDepth, CL, Opts);
format_argument({_Fid, _Required, Type, Name, _Default}, Value, CurDepth, CL, Opts) ->
NameStr = to_string(Name),
NameStrLen = length(NameStr),
{Format, NewCL} = format_thrift_value(Type, Value, CurDepth, CL + NameStrLen + 3, Opts),
{[NameStr, "=", Format], NewCL}.
format_argument({_Fid, _Required, _Type, _Name, undefined}, undefined, Result, _MD, _ML, _MPSL, AD) ->
{Result, AD};
format_argument({Fid, Required, Type, Name, Default}, undefined, Result, MD, ML, MPSL, AD) ->
format_argument({Fid, Required, Type, Name, Default}, Default, Result, MD, ML, MPSL, AD);
format_argument({_Fid, _Required, Type, Name, _Default}, Value, Result0, MD, ML, MPSL, AD) ->
NameStr = atom_to_binary(Name, unicode),
Result1 = maybe_add_delimiter(AD, Result0),
Result2 = <<Result1/binary, NameStr/binary, "=">>,
{format_thrift_value(Type, Value, Result2, MD, ML, MPSL), true}.
-spec format_reply(atom(), atom(), atom(), term(), woody:options()) ->
woody_event_handler:msg().
@ -103,125 +85,99 @@ format_exception(Module, Service, Function, Value, Opts) when is_tuple(Value) ->
ReplyType = get_exception_type(Exception, ExceptionTypeList),
format(ReplyType, Value, normalize_options(Opts));
format_exception(_Module, _Service, _Function, Value, Opts) ->
#{max_length := ML} = normalize_options(Opts),
{"~ts", [io_lib:format("~0tp", [Value], [{chars_limit, ML}])]}.
{"~ts", [format_verbatim(Value, <<>>, normalize_options(Opts))]}.
format(ReplyType, Value, #{max_length := ML} = Opts) ->
format(ReplyType, Value, #{max_length := ML, max_depth := MD, max_pritable_string_length := MPSL}) ->
try
{ReplyFmt, _} = format_thrift_value(ReplyType, Value, 0, 0, Opts),
{"~ts", [ReplyFmt]}
ReplyValue = format_thrift_value(ReplyType, Value, <<>>, MD, ML, MPSL),
{"~ts", [ReplyValue]}
catch
E:R:S ->
WarningDetails = genlib_format:format_exception({E, R, S}),
logger:warning("EVENT FORMATTER ERROR: ~tp", [WarningDetails]),
{"~ts", [io_lib:format("~0tp", [Value], [{chars_limit, ML}])]}
Details = genlib_format:format_exception({E, R, S}),
_ = logger:warning("EVENT FORMATTER ERROR: ~ts", [Details]),
{"~ts", [format_verbatim(Value, <<>>, MD, ML)]}
end.
-spec format_thrift_value(term(), term(), non_neg_integer(), non_neg_integer(), opts()) ->
{iolist(), non_neg_integer()}.
format_thrift_value({struct, struct, []}, Value, _CurDepth, CL, Opts) ->
-spec format_thrift_value(term(), term(), binary(), limit(), limit(), non_neg_integer()) ->
binary().
format_thrift_value(Type, Value, Result, MD, ML, MPSL) ->
try format_thrift_value_(Type, Value, Result, MD, ML, MPSL) catch
error:_ -> format_verbatim(Value, Result, MD, ML)
end.
format_thrift_value_({struct, struct, []}, Value, Result, MD, ML, _MPSL) ->
%% {struct,struct,[]} === thrift's void
%% so just return Value
#{max_length := ML} = normalize_options(Opts),
ValueString = io_lib:format("~0tp", [Value], [{chars_limit, ML}]),
Length = length(ValueString),
{ValueString, CL + Length};
format_thrift_value({struct, struct, {Module, Struct}}, Value, CurDepth, CL, Opts) ->
format_struct(Module, Struct, Value, CurDepth + 1, CL, Opts);
format_thrift_value({struct, union, {Module, Struct}}, Value, CurDepth, CL, Opts) ->
format_union(Module, Struct, Value, CurDepth + 1, CL, Opts);
format_thrift_value({struct, exception, {Module, Struct}}, Value, CurDepth, CL, Opts) ->
format_struct(Module, Struct, Value, CurDepth + 1, CL, Opts);
format_thrift_value({enum, {_Module, _Struct}}, Value, _CurDepth, CL, _Opts) ->
format_verbatim(Value, Result, MD, ML);
format_thrift_value_({struct, struct, {Module, Struct}}, Value, Result, MD, ML, MPSL) ->
format_struct(Module, Struct, Value, Result, dec(MD), ML, MPSL);
format_thrift_value_({struct, union, {Module, Struct}}, Value, Result, MD, ML, MPSL) ->
format_union(Module, Struct, Value, Result, dec(MD), ML, MPSL);
format_thrift_value_({struct, exception, {Module, Struct}}, Value, Result, MD, ML, MPSL) ->
format_struct(Module, Struct, Value, Result, dec(MD), ML, MPSL);
format_thrift_value_({enum, {_Module, _Struct}}, Value, Result, _MD, _ML, _MPSL) ->
ValueString = to_string(Value),
Length = length(ValueString),
{ValueString, CL + Length};
format_thrift_value(string, Value, _CurDepth, CL, Opts) when is_binary(Value) ->
case is_printable(Value, Opts) of
{ok, Slice} ->
ValueString = value_to_string(Slice),
Length = length(ValueString),
{["'", ValueString, "'"], CL + Length + 2}; %% 2 = length("''")
{error, not_printable} ->
Fmt = format_non_printable_string(Value),
Length = length(Fmt),
{Fmt, CL + Length}
<<Result/binary, ValueString/binary>>;
format_thrift_value_(string, Value, Result0, _MD, ML, MPSL) when is_binary(Value) ->
case is_printable(Value, ML, byte_size(Result0), MPSL) of
Slice when is_binary(Slice) ->
Result1 = escape_printable_string(Slice, <<Result0/binary, "'">>),
case byte_size(Slice) == byte_size(Value) of
true -> <<Result1/binary, "'">>;
false -> <<Result1/binary, "...'">>
end;
format_thrift_value(string, Value, _CurDepth, CL, _Opts) ->
ValueString = value_to_string(Value),
Length = length(ValueString),
{["'", ValueString, "'"], CL + Length + 2}; %% 2 = length("''")
format_thrift_value({list, _}, _, CurDepth, CL, #{max_depth := MD})
when MD >= 0, CurDepth >= MD ->
{"[...]", CL + 5}; %% 5 = length("[...]")
format_thrift_value({list, Type}, ValueList, CurDepth, CL, Opts) ->
{Format, CL1} = format_thrift_list(Type, ValueList, CurDepth + 1, CL + 2, Opts),
{["[", Format, "]"], CL1};
format_thrift_value({set, _}, _, CurDepth, CL, #{max_depth := MD})
when MD >= 0, CurDepth >= MD ->
{"{...}", CL + 5}; %% 5 = length("{...}")
format_thrift_value({set, Type}, SetofValues, CurDepth, CL, Opts) ->
{Format, CL1} = format_thrift_set(Type, SetofValues, CurDepth + 1, CL + 2, Opts),
{["{", Format, "}"], CL1};
format_thrift_value({map, _}, _, CurDepth, CL, #{max_depth := MD})
when MD >= 0, CurDepth >= MD ->
{"#{...}", CL + 6}; %% 6 = length("#{...}")
format_thrift_value({map, KeyType, ValueType}, Map, CurDepth, CL, Opts) ->
{Params, CL1} =
format_map(KeyType, ValueType, Map, "", CurDepth + 1, CL + 3, Opts, false),
{["#{", Params, "}"], CL1};
format_thrift_value(bool, false, _CurDepth, CL, _Opts) ->
{"false", CL + 5};
format_thrift_value(bool, true, _CurDepth, CL, _Opts) ->
{"true", CL + 4};
format_thrift_value(_Type, Value, _CurDepth, CL, _Opts) when is_integer(Value) ->
ValueStr = integer_to_list(Value),
Length = length(ValueStr),
{ValueStr, CL + Length};
format_thrift_value(_Type, Value, _CurDepth, CL, _Opts) when is_float(Value) ->
ValueStr = float_to_list(Value),
Length = length(ValueStr),
{ValueStr, CL + Length}.
not_printable ->
format_non_printable_string(Value, Result0)
end;
format_thrift_value_({list, _}, _, Result, 0, _ML, _MPSL) ->
<<Result/binary, "[...]">>;
format_thrift_value_({list, Type}, ValueList, Result0, MD, ML, MPSL) ->
Result1 = format_thrift_list(Type, ValueList, <<Result0/binary, "[">>, dec(MD), dec(ML), MPSL),
<<Result1/binary, "]">>;
format_thrift_value_({set, _}, _, Result, 0, _ML, _MPSL) ->
<<Result/binary, "{...}">>;
format_thrift_value_({set, Type}, SetofValues, Result0, MD, ML, MPSL) ->
Result1 = format_thrift_set(Type, SetofValues, <<Result0/binary, "{">>, dec(MD), dec(ML), MPSL),
<<Result1/binary, "}">>;
format_thrift_value_({map, _}, _, Result, 0, _ML, _MPSL) ->
<<Result/binary, "#{...}">>;
format_thrift_value_({map, KeyType, ValueType}, Map, Result0, MD, ML, MPSL) ->
Result1 = format_map(KeyType, ValueType, Map, <<Result0/binary, "#{">>, dec(MD), dec(ML), MPSL, false),
<<Result1/binary, "}">>;
format_thrift_value_(bool, false, Result, _MD, _ML, _MPSL) ->
<<Result/binary, "false">>;
format_thrift_value_(bool, true, Result, _MD, _ML, _MPSL) ->
<<Result/binary, "true">>;
format_thrift_value_(_Type, Value, Result, _MD, _ML, _MPSL) when is_integer(Value) ->
ValueStr = erlang:integer_to_binary(Value),
<<Result/binary, ValueStr/binary>>;
format_thrift_value_(_Type, Value, Result, _MD, _ML, _MPSL) when is_float(Value) ->
ValueStr = erlang:float_to_binary(Value, [{decimals, ?MAX_FLOAT_DECIMALS}, compact]),
<<Result/binary, ValueStr/binary>>.
format_thrift_list(Type, ValueList, CurDepth, CL, Opts) when length(ValueList) =< ?MAX_PRINTABLE_LIST_LENGTH ->
format_list(Type, ValueList, "", CurDepth, CL, Opts, false);
format_thrift_list(Type, OriginalValueList, CurDepth, CL, #{max_length := ML} = Opts) ->
FirstEntry = hd(OriginalValueList),
LastEntry = lists:last(OriginalValueList),
{FirstEntryFmt, FirstEntryCL} =
format_thrift_value(Type, FirstEntry, CurDepth, CL, Opts),
case FirstEntryCL < ML orelse ML < 0 of
format_thrift_list(Type, ValueList, Result, MD, ML, MPSL) when length(ValueList) =< ?MAX_PRINTABLE_LIST_LENGTH ->
format_list(Type, ValueList, Result, MD, ML, MPSL, false);
format_thrift_list(Type, [FirstEntry | Rest], Result0, MD, ML, MPSL) ->
LastEntry = lists:last(Rest),
Result1 = format_thrift_value_(Type, FirstEntry, Result0, MD, ML, MPSL),
case byte_size(Result1) < ML of
true ->
SkippedLength = length(OriginalValueList) - 2,
SkippedMsg = ["...", integer_to_list(SkippedLength), " more..."],
SkippedMsgLength = erlang:iolist_size(SkippedMsg),
case FirstEntryCL + SkippedMsgLength + 2 < ML orelse ML < 0 of
SkippedLength = erlang:integer_to_binary(length(Rest) - 1),
Result2 = <<Result1/binary, ",...", SkippedLength/binary, " more...,">>,
case byte_size(Result2) < ML of
true ->
{LastEntryFmt, LastEntryCL} =
format_thrift_value(Type, LastEntry, CurDepth, FirstEntryCL + SkippedMsgLength + 4, Opts),
format_list_result(
LastEntryCL < ML orelse ML < 0,
[FirstEntryFmt, ",", SkippedMsg, ",", LastEntryFmt],
LastEntryCL,
[FirstEntryFmt, ",..."],
FirstEntryCL + 4
);
format_thrift_value_(Type, LastEntry, Result2, MD, ML, MPSL);
false ->
{[FirstEntryFmt, ",..."], FirstEntryCL + 4}
<<Result2/binary, "...">>
end;
false ->
{[FirstEntryFmt, ",..."], FirstEntryCL + 4}
<<Result1/binary, ",...">>
end.
format_list_result(true, Fmt, CL, _, _) ->
{Fmt, CL};
format_list_result(false, _, _, Fmt, CL) ->
{Fmt, CL}.
format_thrift_set(Type, SetofValues, CurDepth, CL, Opts) ->
format_thrift_set(Type, SetofValues, Result, MD, ML, MPSL) ->
%% Actually ordsets is a list so leave this implementation as-is
ValueList = ordsets:to_list(SetofValues),
format_list(Type, ValueList, "", CurDepth, CL, Opts, false).
format_list(Type, ValueList, Result, MD, ML, MPSL, false).
get_exception_type(ExceptionRecord, ExceptionTypeList) ->
Result =
@ -241,213 +197,202 @@ get_exception_type(ExceptionRecord, ExceptionTypeList) ->
undefined
end.
-spec format_struct(atom(), atom(), term(), non_neg_integer(), non_neg_integer(), opts()) ->
{iolist(), non_neg_integer()}.
format_struct(_Module, Struct, _StructValue, CurDepth, CL, #{max_depth := MD})
when MD >= 0, CurDepth > MD ->
{[to_string(Struct), "{...}"], CL + 5}; %% 5 = length("{...}")
format_struct(Module, Struct, StructValue, CurDepth, CL, Opts = #{max_length := ML}) ->
-spec format_struct(atom(), atom(), term(), binary(), non_neg_integer(), non_neg_integer(), opts()) ->
binary().
format_struct(_Module, Struct, _StructValue, Result, 0, _ML, _MPSL) ->
StructName = atom_to_binary(Struct, unicode),
<<Result/binary, StructName/binary, "{...}">>;
format_struct(Module, Struct, StructValue, Result0, MD, ML, MPSL) ->
%% struct and exception have same structure
{struct, _, StructMeta} = Module:struct_info(Struct),
ValueList = tl(tuple_to_list(StructValue)), %% Remove record name
case length(StructMeta) == length(ValueList) of
case length(StructMeta) == tuple_size(StructValue) - 1 of
true ->
StructName = to_string(Struct),
StructNameLength = length(StructName),
{Params, NewCL} =
format_struct_(
StructName = atom_to_binary(Struct, unicode),
Result1 = format_struct_(
StructMeta,
ValueList,
"",
CurDepth + 1,
CL + StructNameLength + 2,
Opts,
StructValue,
2,
<<Result0/binary, StructName/binary, "{">>,
MD,
dec(ML),
MPSL,
false
),
{[StructName, "{", Params, "}"], NewCL};
<<Result1/binary, "}">>;
false ->
Length = get_length(ML, CL),
Params = io_lib:format("~p", [StructValue], [{chars_limit, Length}]),
{Params, CL + Length}
% TODO when is this possible?
format_verbatim(StructValue, Result0, MD, ML)
end.
format_struct_([], [], Acc, _CurDepth, CL, _Opts, _AddDelimiter) ->
{Acc, CL};
format_struct_(_Types, _Values, Result, _CurDepth, CL, #{max_length := ML}, AddDelimiter)
when ML >= 0, CL > ML->
Delimiter = maybe_add_delimiter(AddDelimiter),
DelimiterLen = length(Delimiter),
{[Result, Delimiter, "..."], CL + 3 + DelimiterLen};
format_struct_([Type | RestTypes], [Value | RestValues], Acc, CurDepth, CL, Opts, AddDelimiter) ->
case format_argument(Type, Value, CurDepth, CL, Opts) of
{undefined, CL} ->
format_struct_([], S, I, Result, _MD, _ML, _MPSL, _AD) when I > tuple_size(S) ->
Result;
format_struct_(_Types, _S, _I, Result0, _MD, ML, _MPSL, AD) when byte_size(Result0) > ML ->
Result1 = maybe_add_delimiter(AD, Result0),
<<Result1/binary, "...">>;
format_struct_([Type | RestTypes], S, I, Result0, MD, ML, MPSL, AD) ->
Value = erlang:element(I, S),
{Result1, AD1} = format_argument(Type, Value, Result0, MD, ML, MPSL, AD),
format_struct_(
RestTypes,
RestValues,
Acc,
CurDepth,
CL,
Opts,
AddDelimiter
);
{F, CL1} ->
Delimiter = maybe_add_delimiter(AddDelimiter),
DelimiterLen = length(Delimiter),
format_struct_(
RestTypes,
RestValues,
[Acc | [Delimiter, F]],
CurDepth, CL1 + DelimiterLen,
Opts,
true
)
end.
S,
I + 1,
Result1,
MD,
ML,
MPSL,
AD1
).
-spec format_union(atom(), atom(), term(), non_neg_integer(), non_neg_integer(), opts()) ->
{iolist(), non_neg_integer()}.
format_union(_Module, Struct, _StructValue, CurDepth, CL, #{max_depth := MD})
when MD >= 0, CurDepth > MD ->
{[to_string(Struct), "{...}"], CL + 5}; %% 5 = length("{...}"
format_union(Module, Struct, {Type, UnionValue}, CurDepth, CL, Opts) ->
-spec format_union(atom(), atom(), term(), binary(), non_neg_integer(), non_neg_integer(), opts()) ->
binary().
format_union(_Module, Struct, _StructValue, Result, 0, _ML, _MPSL) ->
StructName = atom_to_binary(Struct, unicode),
<<Result/binary, StructName/binary, "{...}">>;
format_union(Module, Struct, {Type, UnionValue}, Result0, MD, ML, MPSL) ->
{struct, union, StructMeta} = Module:struct_info(Struct),
{value, UnionMeta} = lists:keysearch(Type, 4, StructMeta),
StructName = to_string(Struct),
StructNameLen = length(StructName),
{Argument, CL1} = format_argument(
UnionMeta = lists:keyfind(Type, 4, StructMeta),
StructName = atom_to_binary(Struct, unicode),
Result1 = <<Result0/binary, StructName/binary, "{">>,
case byte_size(Result1) > ML of
false ->
{Result2, _} = format_argument(
UnionMeta,
UnionValue,
CurDepth,
CL + StructNameLen + 2,
Opts
), %% 2 = length("{}")
{[StructName, "{", Argument, "}"], CL1}.
Result1,
MD,
dec(ML),
MPSL,
false
),
<<Result2/binary, "}">>;
true ->
<<Result1/binary, "...}">>
end.
format_non_printable_string(Value) ->
Size = byte_size(Value),
["<<", integer_to_list(Size), " bytes>>"].
format_non_printable_string(Value, Result) ->
SizeStr = erlang:integer_to_binary(byte_size(Value)),
<<Result/binary, "<<", SizeStr/binary, " bytes>>">>.
is_printable(<<>> = Slice, _Opts) ->
{ok, Slice};
is_printable(Value, #{max_pritable_string_length := MPSL}) ->
is_printable(<<>> = Slice, _ML, _CL, _MPSL) ->
Slice;
is_printable(Value, ML, CL, MPSL) ->
%% Try to get slice of first MPSL bytes from Value,
%% assuming success means Value is printable string
%% NOTE: Empty result means non-printable Value
ValueSize = byte_size(Value),
try string:slice(Value, 0, MPSL) of
try string:slice(Value, 0, compute_slice_length(ML, CL, MPSL)) of
<<>> ->
{error, not_printable};
Slice when byte_size(Slice) < ValueSize ->
{ok, [Slice, "..."]};
not_printable;
Slice ->
{ok, Slice}
Slice
catch
_:_ ->
%% Completely wrong binary data drives to crash in string internals,
%% mark such data as non-printable instead
{error, not_printable}
not_printable
end.
-spec value_to_string(list() | binary() | atom()) -> list().
value_to_string(S) ->
[maybe_replace(C) || C <- string:to_graphemes(to_string(S))].
dec(unlimited) -> unlimited;
dec(0) -> 0;
dec(N) -> N - 1.
maybe_replace($\') ->
[$\\, $\'];
maybe_replace(C) ->
format_verbatim(Term, Result, #{max_length := ML, max_depth := MD}) ->
format_verbatim(Term, Result, MD, ML).
format_verbatim(Term, Result, MD, ML) ->
Opts = [
{line_length , 0},
{depth , case MD of unlimited -> -1; _ -> MD end},
{chars_limit , case ML of unlimited -> -1; _ -> max_(0, ML - byte_size(Result)) end},
{encoding , unicode}
],
Printout = erlang:iolist_to_binary(io_lib_pretty:print(Term, Opts)),
<<Result/binary, Printout/binary>>.
compute_slice_length(unlimited, _CL, MPSL) ->
MPSL;
compute_slice_length(ML, CL, MPSL) ->
max_(12, min_(ML - CL, MPSL)). % length("<<X bytes>>") = 11
max_(A, B) when A > B -> A;
max_(_, B) -> B.
min_(A, B) when A < B -> A;
min_(_, B) -> B.
-spec escape_printable_string(binary(), binary()) -> binary().
escape_printable_string(<<>>, Result) ->
Result;
escape_printable_string(<<$', String/binary>>, Result) ->
escape_printable_string(String, <<Result/binary, "\\'">>);
escape_printable_string(<<C/utf8, String/binary>>, Result) ->
case unicode_util:is_whitespace(C) of
true -> $\s;
false -> C
true -> escape_printable_string(String, <<Result/binary, " ">>);
false -> escape_printable_string(String, <<Result/binary, C/utf8>>)
end.
-spec to_string(list() | binary() | atom()) -> list().
%% NOTE: Must match to supported types for `~s`
to_string(Value) when is_list(Value) ->
Value;
-spec to_string(binary() | atom()) -> binary().
to_string(Value) when is_binary(Value) ->
unicode:characters_to_list(Value);
Value;
to_string(Value) when is_atom(Value) ->
atom_to_list(Value);
erlang:atom_to_binary(Value, unicode);
to_string(_) ->
error(badarg).
normalize_options(Opts) ->
maps:merge(#{
max_depth => -1,
max_length => -1,
max_depth => unlimited,
max_length => unlimited,
max_pritable_string_length => ?MAX_BIN_SIZE
}, Opts).
maybe_add_delimiter(false) ->
"";
maybe_add_delimiter(true) ->
",".
maybe_add_delimiter(false, Result) ->
Result;
maybe_add_delimiter(true, Result) ->
<<Result/binary, ",">>.
maybe_add_more_marker(false) ->
"";
maybe_add_more_marker(true) ->
"...".
maybe_add_more_marker(false, Result) ->
Result;
maybe_add_more_marker(true, Result) ->
<<Result/binary, "...">>.
get_length(ML, CL) when ML > CL ->
ML - CL;
get_length(_ML, _CL) ->
0.
format_list(_Type, [], Result, _CurDepth, CL, _Opts, _IsFirst) ->
{Result, CL};
format_list(Type, [Entry | ValueList], AccFmt, CurDepth, CL, Opts, IsFirst) ->
#{max_length := ML} = Opts,
Delimiter = maybe_add_delimiter(IsFirst),
DelimiterLen = length(Delimiter),
{Fmt, CL1} = format_thrift_value(Type, Entry, CurDepth, CL + DelimiterLen, Opts),
Result = [AccFmt | [Delimiter, Fmt]],
case CL1 of
CL1 when ML < 0 ->
format_list(Type, ValueList, Result, CurDepth, CL1, Opts, true);
CL1 when CL1 =< ML ->
format_list(Type, ValueList, Result, CurDepth, CL1, Opts, true);
CL1 ->
format_list(_Type, [], Result, _MD, _ML, _MPSL, _AD) ->
Result;
format_list(Type, [Entry | ValueList], Result0, MD, ML, MPSL, AD) ->
Result1 = maybe_add_delimiter(AD, Result0),
Result2 = format_thrift_value_(Type, Entry, Result1, MD, ML, MPSL),
case byte_size(Result2) of
CL when ML > CL ->
format_list(Type, ValueList, Result2, MD, ML, MPSL, true);
_ ->
MaybeAddMoreMarker = length(ValueList) =/= 0,
stop_format(Result, CL1, MaybeAddMoreMarker)
stop_format(MaybeAddMoreMarker, Result2)
end.
format_map(_KeyType, _ValueType, E, Result, _CurDepth, CL, _Opts, _AddDelimiter) when map_size(E) =:= 0 ->
{Result, CL};
format_map(KeyType, ValueType, Map, AccFmt, CurDepth, CL, Opts, AddDelimiter) ->
format_map(_KeyType, _ValueType, Map, Result, _MD, _ML, _MPSL, _AD) when map_size(Map) =:= 0 ->
Result;
format_map(KeyType, ValueType, Map, Result0, MD, ML, MPSL, AD) ->
MapIterator = maps:iterator(Map),
format_map_(KeyType, ValueType, maps:next(MapIterator), AccFmt, CurDepth, CL, Opts, AddDelimiter).
format_map_(KeyType, ValueType, maps:next(MapIterator), Result0, MD, ML, MPSL, AD).
format_map_(KeyType, ValueType, {Key, Value, MapIterator}, AccFmt, CurDepth, CL, Opts, AddDelimiter) ->
{KeyFmt, CL1} = format_thrift_value(KeyType, Key, CurDepth, CL, Opts),
{ValueFmt, CL2} = format_thrift_value(ValueType, Value, CurDepth, CL1, Opts),
#{max_length := ML} = Opts,
MapStr = "=>",
MapStrLen = 2,
Delimiter = maybe_add_delimiter(AddDelimiter),
DelimiterLen = length(Delimiter),
NewCL = CL2 + MapStrLen + DelimiterLen,
Result = [AccFmt | [Delimiter, KeyFmt, MapStr, ValueFmt]],
format_map_(KeyType, ValueType, {Key, Value, MapIterator}, Result0, MD, ML, MPSL, AD) ->
Result1 = maybe_add_delimiter(AD, Result0),
Result2 = format_thrift_value_(KeyType, Key, Result1, MD, ML, MPSL),
Result3 = format_thrift_value_(ValueType, Value, <<Result2/binary, "=">>, MD, ML, MPSL),
Next = maps:next(MapIterator),
case NewCL of
NewCL when ML < 0 ->
format_map_(KeyType, ValueType, Next, Result, CurDepth, NewCL, Opts, true);
NewCL when NewCL =< ML ->
format_map_(KeyType, ValueType, Next, Result, CurDepth, NewCL, Opts, true);
NewCL ->
case byte_size(Result3) of
CL when ML > CL ->
format_map_(KeyType, ValueType, Next, Result3, MD, ML, MPSL, true);
_ ->
MaybeAddMoreMarker = Next =/= none,
stop_format(Result, NewCL, MaybeAddMoreMarker)
stop_format(MaybeAddMoreMarker, Result3)
end;
format_map_(_, _, _, AccFmt, _, CL, _, _) ->
{AccFmt, CL}.
format_map_(_, _, _, Result, _, _, _, _) ->
Result.
stop_format(Result, CL1, MaybeAddMoreMarker) ->
Delimiter1 = maybe_add_delimiter(MaybeAddMoreMarker),
DelimiterLen1 = length(Delimiter1),
MoreMarker = maybe_add_more_marker(MaybeAddMoreMarker),
MoreMarkerLen = length(MoreMarker),
{
[Result | [Delimiter1, MoreMarker]],
CL1 + MoreMarkerLen + DelimiterLen1
}.
stop_format(MaybeAddMoreMarker, Result0) ->
Result1 = maybe_add_delimiter(MaybeAddMoreMarker, Result0),
Result2 = maybe_add_more_marker(MaybeAddMoreMarker, Result1),
Result2.
-ifdef(TEST).
@ -608,7 +553,8 @@ depth_test_() -> [
dmsl_payment_processing_thrift,
'PartyManagement',
'CreateClaim',
?ARGS
?ARGS,
#{}
)
)
),
@ -641,21 +587,6 @@ depth_test_() -> [
"PartyManagement:CreateClaim(party_id='1CR1Xziml7o',changeset=[PartyModification{"
"contract_modification=ContractModificationUnit{...}},...2 more...,"
"PartyModification{shop_modification=ShopModificationUnit{...}}])",
format_msg(
format_call(
dmsl_payment_processing_thrift,
'PartyManagement',
'CreateClaim',
?ARGS,
#{max_depth => 2}
)
)
),
?_assertEqual(
"PartyManagement:CreateClaim(party_id='1CR1Xziml7o',changeset=[PartyModification{"
"contract_modification=ContractModificationUnit{id='1CR1Y2ZcrA0',modification="
"ContractModification{...}}},...2 more...,PartyModification{shop_modification="
"ShopModificationUnit{id='1CR1Y2ZcrA2',modification=ShopModification{...}}}])",
format_msg(
format_call(
dmsl_payment_processing_thrift,
@ -681,23 +612,35 @@ depth_test_() -> [
)
)
),
?_test(
begin
{Result, _} =
format_thrift_value(
{set, string},
ordsets:from_list(["a", "2", "ddd"]),
0, 0, #{max_length => -1, max_depth => -1}
?_assertEqual(
"PartyManagement:CreateClaim(party_id='1CR1Xziml7o',changeset=[PartyModification{"
"contract_modification=ContractModificationUnit{id='1CR1Y2ZcrA0',modification="
"ContractModification{...}}},...2 more...,PartyModification{shop_modification="
"ShopModificationUnit{id='1CR1Y2ZcrA2',modification=ShopModification{...}}}])",
format_msg(
format_call(
dmsl_payment_processing_thrift,
'PartyManagement',
'CreateClaim',
?ARGS,
#{max_depth => 4}
)
)
),
?_assertEqual(
"{'a', '2', 'ddd'}",
lists:flatten(Result)
<<"{'2','a','ddd'}">>,
format_thrift_value(
{set, string},
ordsets:from_list([<<"a">>, <<"2">>, <<"ddd">>]),
<<>>,
unlimited,
unlimited,
?MAX_PRINTABLE_LIST_LENGTH
)
end
),
?_assertEqual(
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{ns='party',"
"id='1CSHThTEJ84',history=[...],history_range=HistoryRange{...},aux_state=Content{...},"
"id='1CSHThTEJ84',history=[Event{...}],history_range=HistoryRange{...},aux_state=Content{...},"
"aux_state_legacy=Value{...}}})",
format_msg(
format_call(
@ -705,22 +648,22 @@ depth_test_() -> [
'Processor',
'ProcessCall',
?ARGS2,
#{max_depth => 4}
#{max_depth => 3}
)
)
),
?_assertEqual(
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{ns='party',"
"id='1CSHThTEJ84',history=[Event{...}],history_range=HistoryRange{limit=10,direction="
"backward},aux_state=Content{data=Value{...}},aux_state_legacy=Value{obj=#{Value{...}=>"
"Value{...},Value{...}=>Value{...}}}}})",
"backward},aux_state=Content{data=Value{...}},aux_state_legacy=Value{obj=#{Value{...}="
"Value{...},Value{...}=Value{...}}}}})",
format_msg(
format_call(
mg_proto_state_processing_thrift,
'Processor',
'ProcessCall',
?ARGS2,
#{max_depth => 5}
#{max_depth => 4}
)
)
)
@ -748,7 +691,7 @@ length_test_() -> [
'PartyManagement',
'CreateClaim',
?ARGS,
#{max_length => -1}
#{max_length => unlimited}
)
)
),
@ -786,7 +729,10 @@ length_test_() -> [
"O\\'Centro, 060...',post_address='NaN',representative_position='Director',"
"representative_full_name='Someone',representative_document='100$ banknote',"
"russian_bank_account=RussianBankAccount{account='4276300010908312893',bank_name="
"'SomeBank',bank_post_account='123129876',bank_bik='66642666'}}}}}}}},...])",
"'SomeBank',bank_post_account='123129876',bank_bik='66642666'}}}}}}}},...2 more...,"
"PartyModification{shop_modification=ShopModificationUnit{id='1CR1Y2ZcrA2',"
"modification=ShopModification{shop_account_creation=ShopAccountParams{"
"currency=CurrencyRef{symbolic_code='RUB'}}}}}])",
format_msg(
format_call(
dmsl_payment_processing_thrift,
@ -816,7 +762,7 @@ length_test_() -> [
"ContractParams{template=ContractTemplateRef{id=1},payment_institution=PaymentInstitutionRef{id=1},"
"contractor=Contractor{legal_entity=LegalEntity{russian_legal_entity=RussianLegalEntity{"
"registered_name='Hoofs & Horns OJSC',registered_number='1234509876',inn='1213456789012',"
"actual_address='Nezahualcoyotl 109 Piso 8, O\\'Centro, 060...',...}}}}}}},...])",
"actual_address='Nezahualcoyotl 109 Piso 8, O...',...}}}}}}},...])",
format_msg(
format_call(
dmsl_payment_processing_thrift,
@ -830,13 +776,13 @@ length_test_() -> [
?_assertEqual(
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{"
"ns='party',id='1CSHThTEJ84',history=[Event{id=1,created_at='2019-08-13T07:52:11.080519Z',"
"data=Value{arr=[Value{obj=#{Value{str='ct'}=>Value{str='application/x-erlang-binary'},"
"Value{str='vsn'}=>Value{i=6}}},Value{bin=<<249 bytes>>}]}}],history_range="
"data=Value{arr=[Value{obj=#{Value{str='ct'}=Value{str='application/x-erlang-binary'},"
"Value{str='vsn'}=Value{i=6}}},Value{bin=<<249 bytes>>}]}}],history_range="
"HistoryRange{limit=10,"
"direction=backward},aux_state=Content{data=Value{obj=#{Value{str='aux_state'}=>"
"Value{bin=<<52 bytes>>},Value{str='ct'}=>Value{str='application/x-erlang-binary'}}}},"
"aux_state_legacy=Value{obj=#{Value{str='aux_state'}=>Value{bin=<<52 bytes>>},Value{str='ct'}"
"=>Value{str='application/x-erlang-binary'}}}}})",
"direction=backward},aux_state=Content{data=Value{obj=#{Value{str='aux_state'}="
"Value{bin=<<52 bytes>>},Value{str='ct'}=Value{str='application/x-erlang-binary'}}}},"
"aux_state_legacy=Value{obj=#{Value{str='aux_state'}=Value{bin=<<52 bytes>>},Value{str='ct'}"
"=Value{str='application/x-erlang-binary'}}}}})",
format_msg(
format_call(
mg_proto_state_processing_thrift,
@ -850,11 +796,12 @@ length_test_() -> [
?_assertEqual(
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{ns='party',"
"id='1CSHThTEJ84',history=[Event{id=1,created_at='2019-08-13T07:52:11.080519Z',data="
"Value{arr=[Value{obj=#{Value{str='ct'}=>Value{str='application/x-erlang-binary'},"
"Value{str='vsn'}=>Value{i=6}}},Value{bin=<<249 bytes>>}]}}],history_range="
"Value{arr=[Value{obj=#{Value{str='ct'}=Value{str='application/x-erlang-binary'},"
"Value{str='vsn'}=Value{i=6}}},Value{bin=<<249 bytes>>}]}}],history_range="
"HistoryRange{limit=10,"
"direction=backward},aux_state=Content{data=Value{obj=#{Value{str='aux_state'}=>"
"Value{bin=<<52 bytes>>},Value{str='ct'}=>Value{str='application/x-erlang-binary'}}}},...}})",
"direction=backward},aux_state=Content{data=Value{obj=#{Value{str='aux_state'}="
"Value{bin=<<52 bytes>>},Value{str='ct'}=Value{str='application/x-erlang-binary'}}}},"
"aux_state_legacy=Value{...}}})",
format_msg(
format_call(
mg_proto_state_processing_thrift,
@ -868,8 +815,7 @@ length_test_() -> [
?_assertEqual(
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{ns='party',"
"id='1CSHThTEJ84',history=[Event{id=1,created_at='2019-08-13T07:52:11.080519Z',data="
"Value{arr=[Value{obj=#{Value{str='ct'}=>Value{str='application/x-erlang-binary'},...}},"
"...]}}],...}})",
"Value{arr=[Value{obj=#{Value{...}=Value{...},...}},...]}}],...}})",
format_msg(
format_call(
mg_proto_state_processing_thrift,
@ -895,8 +841,8 @@ length_test_() -> [
)
].
-spec depth_and_lenght_test_() -> _.
depth_and_lenght_test_() -> [
-spec depth_and_length_test_() -> _.
depth_and_length_test_() -> [
?_assertEqual(
"PartyManagement:CreateClaim(party_id='1CR1Xziml7o',changeset=[PartyModification{contract_modification"
"=ContractModificationUnit{id='1CR1Y2ZcrA0',modification=ContractModification{creation="
@ -926,13 +872,13 @@ depth_and_lenght_test_() -> [
'PartyManagement',
'CreateClaim',
?ARGS,
#{max_length => 2048, max_depth => 7}
#{max_length => 2048, max_depth => 6}
)
)
),
?_assertEqual(
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{ns='party',"
"id='1CSHThTEJ84',history=[...],history_range=HistoryRange{...},aux_state=Content{...},"
"id='1CSHThTEJ84',history=[Event{...}],history_range=HistoryRange{...},aux_state=Content{...},"
"aux_state_legacy=Value{...}}})",
format_msg(
format_call(
@ -948,14 +894,34 @@ depth_and_lenght_test_() -> [
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{ns='party',"
"id='1CSHThTEJ84',history=[Event{...}],history_range=HistoryRange{limit=10,"
"direction=backward},aux_state=Content{data=Value{...}},aux_state_legacy=Value{obj="
"#{Value{...}=>Value{...},Value{...}=>Value{...}}}}})",
"#{Value{...}=Value{...},Value{...}=Value{...}}}}})",
format_msg(
format_call(
mg_proto_state_processing_thrift,
'Processor',
'ProcessCall',
?ARGS2,
#{max_length => 512, max_depth => 5}
#{max_length => 512, max_depth => 4}
)
)
)
].
-spec verbatim_test_() -> _.
verbatim_test_() -> [
?_assertEqual(
"PartyManagement:CallMissingFunction("
"[undefined,<<\"1CR1Xziml7o\">>,[{contract_modification,{payproc_ContractModificationUnit,"
"<<\"1CR1\"...>>,{...}}},{contract_modification,{payproc_ContractModificationUnit,<<...>>,"
"...}},{shop_modification,{payproc_ShopModificationUnit,...}}|...]]"
")",
format_msg(
format_call(
dmsl_payment_processing_thrift,
'PartyManagement',
'CallMissingFunction',
?ARGS,
#{max_length => 256}
)
)
)

View File

@ -699,7 +699,9 @@ format_service_request_test_() -> [
"Centro, 06082...',post_address='NaN',representative_position='Director',"
"representative_full_name='Someone',representative_document='100$ banknote',"
"russian_bank_account=RussianBankAccount{account='4276300010908312893',bank_name='SomeBank',"
"bank_post_account='123129876',bank_bik='66642666'}}}}}}}},...])",
"bank_post_account='123129876',bank_bik='66642666'}}}}}}}},...2 more...,"
"PartyModification{shop_modification=ShopModificationUnit{id='1CR1Y2ZcrA2',"
"modification=ShopModification{shop_account_creation=ShopA...",
format_msg_limited(
format_event(
?EV_CALL_SERVICE,
@ -762,12 +764,12 @@ format_service_request_test_() -> [
"[1012689088739803136 1012689108264288256 1012689088534282240][client] calling "
"Processor:ProcessCall(a=CallArgs{arg=Value{bin=<<732 bytes>>},machine=Machine{ns='party',"
"id='1CSHThTEJ84',history=[Event{id=1,created_at='2019-08-13T07:52:11.080519Z',"
"data=Value{arr=[Value{obj=#{Value{str='ct'}=>Value{str='application/x-erlang-binary'},"
"Value{str='vsn'}=>Value{i=6}}},Value{bin=<<249 bytes>>}]}}],history_range=HistoryRange{"
"data=Value{arr=[Value{obj=#{Value{str='ct'}=Value{str='application/x-erlang-binary'},"
"Value{str='vsn'}=Value{i=6}}},Value{bin=<<249 bytes>>}]}}],history_range=HistoryRange{"
"limit=10,direction=backward},aux_state=Content{data=Value{obj=#{Value{str='aux_state'}"
"=>Value{bin=<<52 bytes>>},Value{str='ct'}=>Value{str='application/x-erlang-binary'}}}},"
"aux_state_legacy=Value{obj=#{Value{str='aux_state'}=>Value{bin=<<52 bytes>>},Value{str='ct'}"
"=>Value{str='application/x-erlang-binary'}}}}})",
"=Value{bin=<<52 bytes>>},Value{str='ct'}=Value{str='application/x-erlang-binary'}}}},"
"aux_state_legacy=Value{obj=#{Value{str='aux_state'}=Value{bin=<<52 bytes>>},Value{str='ct'}"
"=Value{str='application/x-erlang-binary'}}}}})",
format_msg_limited(
format_event(
?EV_CALL_SERVICE,
@ -894,7 +896,7 @@ format_service_request_with_limit_test_() -> [
"russian_bank_account=RussianBankAccount{account='4276300010908312893',bank_name='SomeBank',"
"bank_post_account='123129876',bank_bik='66642666'}}}}}}}},...2 more...,"
"PartyModification{shop_modification=ShopModificationUnit{id='1CR1Y2ZcrA2',modification=ShopModification"
"{shop_account_creation=Shop...)",
"{shop_account_creation=ShopA...",
format_msg_limited(
format_event(
?EV_CALL_SERVICE,
@ -961,9 +963,9 @@ result_test_() -> [
?_assertEqual(
"[1012689088739803136 1012689108264288256 1012689088534282240][client] request handled successfully: "
"CallResult{response=Value{bin=<<6 bytes>>},change=MachineStateChange{aux_state="
"Content{data=Value{obj=#{Value{str='aux_state'}=>Value{bin=<<108 bytes>>},Value{str='ct'}=>"
"Content{data=Value{obj=#{Value{str='aux_state'}=Value{bin=<<108 bytes>>},Value{str='ct'}="
"Value{str='application/x-erlang-binary'}}}},events=[Content{data=Value{arr=[Value{obj="
"#{Value{str='ct'}=>Value{str='application/x-erlang-binary'},Value{str='vsn'}=>Value{i=6}}},"
"#{Value{str='ct'}=Value{str='application/x-erlang-binary'},Value{str='vsn'}=Value{i=6}}},"
"Value{bin=<<240 bytes>>}]}}]},action=ComplexAction{}}",
format_msg_limited(
format_event(
@ -1045,15 +1047,15 @@ result_test_() -> [
"Party{id='1CSWG2vduGe',contact_info=PartyContactInfo{email='hg_ct_helper'},created_at="
"'2019-08-13T11:19:01.249440Z',blocking=Blocking{unblocked=Unblocked{reason='',since="
"'2019-08-13T11:19:02.655869Z'}},suspension=Suspension{active=Active{since="
"'2019-08-13T11:19:02.891892Z'}},contractors=#{},contracts=#{'1CSWG8j04wK'=>Contract{id="
"'2019-08-13T11:19:02.891892Z'}},contractors=#{},contracts=#{'1CSWG8j04wK'=Contract{id="
"'1CSWG8j04wK',payment_institution=PaymentInstitutionRef{id=1},created_at="
"'2019-08-13T11:19:01.387269Z',status=ContractStatus{active=ContractActive{}},terms="
"TermSetHierarchyRef{id=1},adjustments=[],payout_tools=[PayoutTool{id='1CSWG8j04wL',"
"created_at='2019-08-13T11:19:01.387269Z',currency=CurrencyRef{symbolic_code='RUB'},"
"payout_tool_info=PayoutToolInfo{russian_bank_account=RussianBankAccount{account="
"'4276300010908312893',bank_name='SomeBank',bank_post_account='123129876',bank_bik="
"'66642666'}}}],contractor=Contractor{legal_entity=LegalEntity{russian_legal_entity=RussianLegalEntity{...}"
"...",
"'66642666'}}}],contractor=Contractor{legal_entity=LegalEntity{russian_legal_entity="
"RussianLegalEntity{regis...",
format_msg_limited(
format_event(
?EV_SERVICE_RESULT,
@ -1126,11 +1128,11 @@ result_test_() -> [
"successfully: "
"SignalResult{change=MachineStateChange{aux_state=Content{data=Value{obj=#{}}},"
"events=[Content{data=Value{arr=[Value{arr=[Value{i=2},Value{obj=#{Value{"
"str='change'}=>Value{str='created'},Value{str='contact_info'}=>Value{obj=#{Value{"
"str='email'}=>Value{str='create_customer'}}},Value{str='created_at'}=>Value{"
"str='2019-08-13T11:19:03.714218Z'},Value{str='customer_id'}=>Value{str='1CSWGJ3N8Ns'},"
"Value{str='metadata'}=>Value{nl=Nil{}},Value{str='owner_id'}=>Value{str='1CSWG2vduGe'},"
"Value{str='shop_id'}=>Value{str='1CSWG8j04wM'}}}]}]}}]},action=ComplexAction{}}",
"str='change'}=Value{str='created'},Value{str='contact_info'}=Value{obj=#{Value{"
"str='email'}=Value{str='create_customer'}}},Value{str='created_at'}=Value{"
"str='2019-08-13T11:19:03.714218Z'},Value{str='customer_id'}=Value{str='1CSWGJ3N8Ns'},"
"Value{str='metadata'}=Value{nl=Nil{}},Value{str='owner_id'}=Value{str='1CSWG2vduGe'},"
"Value{str='shop_id'}=Value{str='1CSWG8j04wM'}}}]}]}}]},action=ComplexAction{}}",
format_msg_limited(
format_event(
?EV_SERVICE_RESULT,

View File

@ -2,15 +2,19 @@
%% API
-export([
bench_event_handler/2
iolib_formatter/1,
bench_iolib_formatter/2,
thrift_formatter/1,
bench_thrift_formatter/2
]).
-spec bench_event_handler(term(), term()) -> term().
bench_event_handler(_, _) ->
woody_event_handler:format_event(
'call service',
#{args =>
[{mg_stateproc_CallArgs,
-type input() :: {woody_thrift_formatter:event_meta(), woody:rpc_id()}.
-spec input() ->
input().
input() ->
Meta = #{
args => [{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,
@ -106,9 +110,37 @@ bench_event_handler(_, _) ->
role => server,
service => 'Processor',
service_schema => {mg_proto_state_processing_thrift, 'Processor'},
type => call},
#{
type => call
},
RpcID = #{
span_id => <<"1012689088534282240">>,
trace_id => <<"1012689088739803136">>,
parent_id => <<"1012689108264288256">>}
).
parent_id => <<"1012689108264288256">>
},
{Meta, RpcID}.
-spec iolib_formatter({input, _State}) ->
input().
iolib_formatter({input, _}) ->
input().
-spec thrift_formatter({input, _State}) ->
input().
thrift_formatter({input, _}) ->
input().
-spec bench_iolib_formatter(input(), _State) ->
term().
bench_iolib_formatter({Meta, RpcID}, _) ->
format_msg(format_event_iolib(Meta, RpcID)).
format_event_iolib(#{service := Service, function := Function, args := Args}, _RpcID) ->
{info, {"calling ~0p:~0p(~0tp)", [Service, Function, Args]}}.
-spec bench_thrift_formatter(input(), _State) ->
term().
bench_thrift_formatter({Meta, RpcID}, _) ->
format_msg(woody_event_handler:format_event('call service', Meta, RpcID)).
format_msg({_Severity, {Format, Params}}) ->
io_lib:format(Format, Params).

View File

@ -0,0 +1,45 @@
-module(bench_woody_formatter).
%% API
-export([
iolib_formatter/1,
bench_iolib_formatter/2,
thrift_formatter/1,
bench_thrift_formatter/2
]).
-type input() :: term().
-spec input() ->
input().
input() ->
%% NOTE
%% You will need some reasonably complex term following `domain_config.Snapshot` thrift schema
%% stored in ETF in `test/snapshot.term` for this benchmark to run. It's NOT supplied by
%% default.
{ok, Bin} = file:read_file("test/snapshot.term"),
erlang:binary_to_term(Bin).
-spec iolib_formatter({input, _State}) ->
input().
iolib_formatter({input, _}) ->
input().
-spec thrift_formatter({input, _State}) ->
input().
thrift_formatter({input, _}) ->
input().
-spec bench_iolib_formatter(input(), _State) ->
term().
bench_iolib_formatter(Snapshot, _) ->
format_msg({"~0tp", [Snapshot]}).
-spec bench_thrift_formatter(input(), _State) ->
term().
bench_thrift_formatter(Snapshot, _) ->
Service = dmsl_domain_config_thrift,
format_msg(woody_event_formatter:format_reply(Service, 'Repository', 'Checkout', Snapshot, #{})).
format_msg({Format, Params}) ->
io_lib:format(Format, Params).

View File

@ -0,0 +1,60 @@
-module(benchmark_memory_pressure).
-export([run/0]).
-spec run() ->
ok.
run() ->
Input = input(),
Opts = #{iterations => 10},
_ = run(iolib, mk_iolib_runner(Input), Opts),
_ = run(thrift, mk_thrift_runner(Input), Opts),
ok.
-spec run(atom(), meter_memory_pressure:runner(), meter_memory_pressure:opts()) ->
ok.
run(Name, Runner, Opts) ->
_ = io:format("Benchmarking '~s' memory pressure...~n", [Name]),
_ = io:format("====================================~n", []),
Metrics = meter_memory_pressure:measure(Runner, Opts),
lists:foreach(
fun (Metric) ->
io:format("~24s = ~-16b~n", [Metric, maps:get(Metric, Metrics)])
end,
[
minor_gcs,
minor_gcs_duration,
major_gcs,
major_gcs_duration,
heap_reclaimed,
offheap_bin_reclaimed,
stack_min,
stack_max
]
),
_ = io:format("====================================~n~n", []),
ok.
-spec input() ->
term().
input() ->
%% NOTE
%% You will need some reasonably complex term following `domain_config.Snapshot` thrift schema
%% stored in ETF in `test/snapshot.term` for this benchmark to run. It's NOT supplied by
%% default.
{ok, Binary} = file:read_file("test/snapshot.term"),
erlang:binary_to_term(Binary).
-spec mk_iolib_runner(term()) ->
meter_memory_pressure:runner().
mk_iolib_runner(Snapshot) ->
fun () ->
bench_woody_formatter:bench_iolib_formatter(Snapshot, [])
end.
-spec mk_thrift_runner(term()) ->
meter_memory_pressure:runner().
mk_thrift_runner(Snapshot) ->
fun () ->
bench_woody_formatter:bench_thrift_formatter(Snapshot, [])
end.

View File

@ -0,0 +1,220 @@
-module(meter_memory_pressure).
-type microseconds() :: non_neg_integer().
-type words() :: non_neg_integer().
-type metrics() :: #{
minor_gcs := non_neg_integer(),
minor_gcs_duration := microseconds(),
major_gcs := non_neg_integer(),
minor_gcs_duration := microseconds(),
heap_reclaimed := words(),
offheap_bin_reclaimed := words(),
stack_min := words(),
stack_max := words()
}.
-export([measure/2]).
-export([export/3]).
-export_type([metrics/0]).
%%
-type runner() :: fun(() -> _).
-type opts() :: #{
iterations => pos_integer(),
spawn_opts => [{atom(), _}],
dump_traces => file:filename()
}.
-export_type([runner/0]).
-export_type([opts/0]).
-spec measure(runner(), opts()) ->
metrics().
measure(Runner, Opts0) ->
Opts = maps:merge(get_default_opts(), Opts0),
Token = make_ref(),
Tracer = start_tracer(Token, Opts),
ok = run(Runner, Tracer, Opts),
Metrics = collect_metrics(Tracer, Token),
Metrics.
get_default_opts() ->
#{
iterations => 100,
spawn_opts => [{fullsweep_after, 0}]
}.
run(Runner, Tracer, Opts) ->
SpawnOpts = [monitor, {priority, high}] ++ maps:get(spawn_opts, Opts),
{Staging, MRef} = erlang:spawn_opt(
fun () -> run_staging(Runner, Tracer, Opts) end,
SpawnOpts
),
receive
{'DOWN', MRef, process, Staging, normal} ->
ok
end.
run_staging(Runner, Tracer, Opts) ->
N = maps:get(iterations, Opts),
TraceOpts = [garbage_collection, timestamp, {tracer, Tracer}],
_ = erlang:trace(self(), true, TraceOpts),
iterate(Runner, N).
iterate(Runner, N) when N > 0 ->
_ = Runner(),
iterate(Runner, N - 1);
iterate(_Runner, 0) ->
ok.
%%
start_tracer(Token, Opts) ->
Self = self(),
erlang:spawn_link(fun () -> run_tracer(Self, Token, Opts) end).
collect_metrics(Tracer, Token) ->
_ = Tracer ! Token,
receive
{?MODULE, {metrics, Metrics}} ->
Metrics
end.
run_tracer(MeterPid, Token, Opts) ->
_ = receive Token -> ok end,
Traces = collect_traces(),
Metrics = analyze_traces(Traces),
ok = maybe_dump_traces(Traces, Opts),
MeterPid ! {?MODULE, {metrics, Metrics}}.
collect_traces() ->
collect_traces([]).
collect_traces(Acc) ->
receive
{trace_ts, _Pid, Trace, Info, Clock} ->
collect_traces([{Trace, Info, Clock} | Acc]);
Unexpected ->
error({unexpected, Unexpected})
after
0 ->
lists:reverse(Acc)
end.
maybe_dump_traces(Traces, #{dump_traces := Filename}) ->
file:write_file(Filename, erlang:term_to_binary(Traces));
maybe_dump_traces(_, #{}) ->
ok.
analyze_traces(Traces) ->
analyze_traces(Traces, #{
minor_gcs => 0,
minor_gcs_duration => 0,
major_gcs => 0,
major_gcs_duration => 0,
heap_reclaimed => 0,
offheap_bin_reclaimed => 0
}).
analyze_traces([{gc_minor_start, InfoStart, C1}, {gc_minor_end, InfoEnd, C2} | Rest], M0) ->
M1 = increment(minor_gcs, M0),
M2 = increment(minor_gcs_duration, timer:now_diff(C2, C1), M1),
analyze_traces(Rest, analyze_gc(InfoStart, InfoEnd, M2));
analyze_traces([{gc_major_start, InfoStart, C1}, {gc_major_end, InfoEnd, C2} | Rest], M0) ->
M1 = increment(major_gcs, M0),
M2 = increment(major_gcs_duration, timer:now_diff(C2, C1), M1),
analyze_traces(Rest, analyze_gc(InfoStart, InfoEnd, M2));
analyze_traces([], M) ->
M.
analyze_gc(InfoStart, InfoEnd, M0) ->
M1 = increment(heap_reclaimed, difference(heap_size, InfoEnd, InfoStart), M0),
M2 = increment(offheap_bin_reclaimed, difference(bin_vheap_size, InfoEnd, InfoStart), M1),
M3 = update(stack_min, fun erlang:min/2, min(stack_size, InfoStart, InfoEnd), M2),
M4 = update(stack_max, fun erlang:max/2, max(stack_size, InfoStart, InfoEnd), M3),
M4.
difference(Name, Info1, Info2) ->
combine(Name, fun (V1, V2) -> erlang:max(0, V2 - V1) end, Info1, Info2).
min(Name, Info1, Info2) ->
combine(Name, fun erlang:min/2, Info1, Info2).
max(Name, Info1, Info2) ->
combine(Name, fun erlang:max/2, Info1, Info2).
combine(Name, Fun, Info1, Info2) ->
{_Name, V1} = lists:keyfind(Name, 1, Info1),
{_Name, V2} = lists:keyfind(Name, 1, Info2),
Fun(V1, V2).
increment(Name, Metrics) ->
increment(Name, 1, Metrics).
increment(Name, Delta, Metrics) ->
maps:update_with(Name, fun (V) -> V + Delta end, Metrics).
update(Name, Fun, I, Metrics) ->
maps:update_with(Name, fun (V) -> Fun(V, I) end, I, Metrics).
%%
-spec export(file:filename(), file:filename(), csv) -> ok.
export(FilenameIn, FilenameOut, Format) ->
{ok, Content} = file:read_file(FilenameIn),
Traces = erlang:binary_to_term(Content),
{ok, FileOut} = file:open(FilenameOut, [write, binary]),
ok = format_traces(Traces, Format, FileOut),
ok = file:close(FileOut).
format_traces(Traces, csv, FileOut) ->
_ = format_csv_header(FileOut),
_ = lists:foreach(fun (T) -> format_csv_trace(T, FileOut) end, Traces),
ok.
format_csv_header(Out) ->
Line = " ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s , ~s ~n",
io:fwrite(Out, Line, [
"Time",
"End?",
"Major?",
"Stack",
"Heap",
"HeapBlock",
"BinHeap",
"BinHeapBlock",
"OldHeap",
"OldHeapBlock",
"OldBinHeap",
"OldBinHeapBlock"
]).
format_csv_trace({Event, Info, Clock}, Out) ->
Line = " ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B , ~B ~n",
io:fwrite(Out, Line, [
clock_to_mcs(Clock),
bool_to_integer(lists:member(Event, [gc_minor_end, gc_major_end])),
bool_to_integer(lists:member(Event, [gc_major_start, gc_major_end])),
get_info(stack_size, Info),
get_info(heap_size, Info),
get_info(heap_block_size, Info),
get_info(bin_vheap_size, Info),
get_info(bin_vheap_block_size, Info),
get_info(old_heap_size, Info),
get_info(old_heap_block_size, Info),
get_info(bin_old_vheap_size, Info),
get_info(bin_old_vheap_block_size, Info)
]).
get_info(Name, Info) ->
{_Name, V} = lists:keyfind(Name, 1, Info), V.
clock_to_mcs({MSec, Sec, USec}) ->
(MSec * 1000000 + Sec) * 1000000 + USec.
bool_to_integer(false) ->
0;
bool_to_integer(true) ->
1.

View File

@ -1,6 +1,7 @@
-module(woody_tests_SUITE).
-include_lib("common_test/include/ct.hrl").
-include_lib("stdlib/include/assert.hrl").
-include_lib("hackney/include/hackney_lib.hrl").
-include("woody_test_thrift.hrl").
@ -617,10 +618,10 @@ call_ok_test(_) ->
call_resolver_nxdomain(_) ->
Context = make_context(<<"nxdomain">>),
try call(Context, 'The Void', get_weapon, [<<"Enforcer">>, self_to_bin()])
catch
error:{woody_error, {internal, resource_unavailable, <<"{resolve_failed,nxdomain}">>}} -> ok
end.
?assertError(
{woody_error, {internal, resource_unavailable, <<"{resolve_failed,nxdomain}">>}},
call(Context, 'The Void', get_weapon, [<<"Enforcer">>, self_to_bin()])
).
call3_ok_test(_) ->
{Url, Service} = get_service_endpoint('Weapons'),
@ -647,37 +648,35 @@ call_throw_unexpected_test(_) ->
Id = <<"call_throw_unexpected">>,
Current = genlib_map:get(<<"Rocket Launcher">>, ?WEAPONS),
Context = make_context(Id),
try call(Context, 'Weapons', switch_weapon, [Current, next, 1, self_to_bin()])
catch
error:{woody_error, {external, result_unexpected, _}} -> ok
end,
{ok, _} = receive_msg(Current, Context).
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Weapons', switch_weapon, [Current, next, 1, self_to_bin()])
).
call_system_external_error_test(_) ->
Id = <<"call_system_external_error">>,
Gun = <<"The Ultimate Super Mega Destroyer">>,
Context = make_context(Id),
try call(Context, 'Weapons', get_weapon, [Gun, self_to_bin()])
catch
error:{woody_error, {external, result_unexpected, _}} -> ok
end,
{ok, _} = receive_msg(Gun, Context).
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Weapons', get_weapon, [Gun, self_to_bin()])
).
call_client_error_test(_) ->
Gun = 'Wrong Type of Mega Destroyer',
Context = make_context(<<"call_client_error">>),
try call(Context, 'Weapons', get_weapon, [Gun, self_to_bin()])
catch
error:{woody_error, {internal, result_unexpected, <<"client thrift error: ", _/binary>>}} -> ok
end.
?assertError(
{woody_error, {internal, result_unexpected, <<"client thrift error: ", _/binary>>}},
call(Context, 'Weapons', get_weapon, [Gun, self_to_bin()])
).
call_server_internal_error_test(_) ->
Armor = <<"Helmet">>,
Context = make_context(<<"call_server_internal_error">>),
try call(Context, 'Powerups', get_powerup, [Armor, self_to_bin()])
catch
error:{woody_error, {external, result_unexpected, _}} -> ok
end,
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Powerups', get_powerup, [Armor, self_to_bin()])
),
{ok, _} = receive_msg(Armor, Context).
call_oneway_void_test(_) ->
@ -717,21 +716,19 @@ call_pass_thru_except_test(_) ->
call_pass_thru_bad_result_test(_) ->
Armor = <<"AntiGrav Boots">>,
Context = make_context(<<"call_pass_thru_bad_result">>),
try call(Context, 'Powerups', bad_proxy_get_powerup, [Armor, self_to_bin()])
catch
error:{woody_error, {external, result_unexpected, _}} ->
ok
end,
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Powerups', bad_proxy_get_powerup, [Armor, self_to_bin()])
),
{ok, _} = receive_msg(Armor, Context).
call_pass_thru_bad_except_test(_) ->
Armor = <<"Shield Belt">>,
Context = make_context(<<"call_pass_thru_bad_except">>),
try call(Context, 'Powerups', bad_proxy_get_powerup, [Armor, self_to_bin()])
catch
error:{woody_error, {external, result_unexpected, _}} ->
ok
end,
?assertError(
{woody_error, {external, result_unexpected, _}},
call(Context, 'Powerups', bad_proxy_get_powerup, [Armor, self_to_bin()])
),
{ok, _} = receive_msg(Armor, Context).
call_pass_thru_result_unexpected_test(_) ->
@ -749,11 +746,11 @@ call_pass_thru_result_unknown_test(_) ->
call_pass_thru_error(Id, Powerup, ExceptClass, ErrClass) ->
RpcId = woody_context:new_rpc_id(?ROOT_REQ_PARENT_ID, Id, Id),
Context = woody_context:new(RpcId),
try call(Context, 'Powerups', proxy_get_powerup, [Powerup, self_to_bin()])
catch
ExceptClass:{woody_error, {external, ErrClass, _}} ->
ok
end,
?assertException(
ExceptClass,
{woody_error, {external, ErrClass, _}},
call(Context, 'Powerups', proxy_get_powerup, [Powerup, self_to_bin()])
),
{ok, _} = receive_msg(Powerup, Context).
call_no_headers_404_test(_) ->
@ -776,12 +773,11 @@ call_fail_w_no_headers(Id, Class, Code) ->
Context = make_context(Id),
{Url, Service} = get_service_endpoint('Weapons'),
BinCode = integer_to_binary(Code),
try woody_client:call({Service, get_weapon, [Gun, self_to_bin()]},
?assertError(
{woody_error, {external, Class, <<"got response with http code ", BinCode:3/binary, _/binary>>}},
woody_client:call({Service, get_weapon, [Gun, self_to_bin()]},
#{url => Url, event_handler => ?MODULE}, Context)
catch
error:{woody_error, {external, Class, <<"got response with http code ", BinCode:3/binary, _/binary>>}} ->
ok
end.
).
find_multiple_pools_test(_) ->
true = is_pid(hackney_pool:find_pool(swords)),
@ -824,10 +820,10 @@ call_deadline_reached_on_client_test(_) ->
Opts = #{url => Url, event_handler => ?MODULE},
Deadline = woody_deadline:from_timeout(0),
Context = woody_context:new(Id, #{<<"sleep">> => <<"1000">>}, Deadline),
try woody_client:call(Request, Opts, Context)
catch
error:{woody_error, {internal, resource_unavailable, <<"deadline reached">>}} -> ok
end.
?assertError(
{woody_error, {internal, resource_unavailable, <<"deadline reached">>}},
woody_client:call(Request, Opts, Context)
).
call_deadline_timeout_test(_) ->
Id = <<"call_deadline_timeout">>,
@ -837,16 +833,14 @@ call_deadline_timeout_test(_) ->
Opts = #{url => Url, event_handler => ?MODULE},
Deadline = woody_deadline:from_timeout(500),
Context = woody_context:new(Id, #{<<"sleep">> => <<"3000">>}, Deadline),
try woody_client:call(Request, Opts, Context)
catch
error:{woody_error, {external, result_unknown, <<"timeout">>}} -> ok
end,
try woody_client:call(Request, Opts, Context)
catch
error:{woody_error, {internal, resource_unavailable, <<"deadline reached">>}} -> ok
end.
?assertError(
{woody_error, {external, result_unknown, <<"timeout">>}},
woody_client:call(Request, Opts, Context)
),
?assertError(
{woody_error, {internal, resource_unavailable, <<"deadline reached">>}},
woody_client:call(Request, Opts, Context)
).
server_http_req_validation_test(Config) ->
HeadersMode = proplists:get_value(client_headers_mode, Config),