ED-65: Allow to force woody request timeout (#9)

* Switch to rbkmoney/image-build-erlang@1aa9b46
* Bump to rbkmoney/woody_erlang@f2cd308
* Bump to erlfmt 0.11.0
* Switch to rebar3_lint 0.3.2
This commit is contained in:
Andrew Mayorov 2021-03-03 13:47:44 +03:00 committed by GitHub
parent 36cb53a7d4
commit 2c083eb112
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 178 additions and 150 deletions

View File

@ -8,7 +8,8 @@ TEMPLATES_PATH := .
# Name of the service
SERVICE_NAME := bouncer_client_erlang
BUILD_IMAGE_TAG := 0c638a682f4735a65ef232b81ed872ba494574c3
BUILD_IMAGE_NAME := build-erlang
BUILD_IMAGE_TAG := 1aa9b46b343bcf3bf0d4359ceca1a7ab6d577699
CALL_ANYWHERE := \
submodules \
@ -37,7 +38,7 @@ xref:
$(REBAR) xref
lint:
elvis rock
$(REBAR) lint
check_format:
$(REBAR) fmt -c

View File

@ -1,55 +0,0 @@
[
{elvis, [
{config, [
#{
dirs => [
"src",
"test"
],
filter => "*.erl",
ignore => ["_SUITE.erl$"],
rules => [
{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 => 4}},
{elvis_style, god_modules, #{limit => 25}},
{elvis_style, no_if_expression},
{elvis_style, invalid_dynamic_call, #{ignore => []}},
{elvis_style, used_ignored_variable},
{elvis_style, no_behavior_info},
{elvis_style, module_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*(_SUITE)?$"}},
{elvis_style, function_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*$"}},
{elvis_style, state_record_and_type},
{elvis_style, no_spec_with_records},
{elvis_style, dont_repeat_yourself, #{
min_complexity => 30,
ignore => []
}},
{elvis_style, no_debug_call, #{}}
]
},
#{
dirs => ["."],
filter => "Makefile",
ruleset => makefiles
},
#{
dirs => ["."],
filter => "elvis.config",
ruleset => elvis_config
},
#{
dirs => ["."],
filter => "rebar.config",
rules => [
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_style, no_tabs},
{elvis_style, no_trailing_whitespace}
]
}
]}
]}
].

View File

@ -1,6 +1,5 @@
%% Common project erlang options.
{erl_opts, [
% mandatory
debug_info,
warnings_as_errors,
@ -27,31 +26,11 @@
%% Common project dependencies.
{deps, [
{genlib,
{git, "https://github.com/rbkmoney/genlib.git",
{branch, "master"}
}
},
{bouncer_proto,
{git, "git@github.com:rbkmoney/bouncer-proto.git",
{branch, "master"}
}
},
{org_management_proto,
{git, "git@github.com:rbkmoney/org-management-proto.git",
{branch, "master"}
}
},
{scoper,
{git, "git@github.com:rbkmoney/scoper.git",
{branch, master}
}
},
{woody,
{git, "git@github.com:rbkmoney/woody_erlang.git",
{branch, master}
}
}
{genlib, {git, "https://github.com/rbkmoney/genlib.git", {branch, "master"}}},
{bouncer_proto, {git, "https://github.com/rbkmoney/bouncer-proto", {branch, "master"}}},
{org_management_proto, {git, "git@github.com:rbkmoney/org-management-proto.git", {branch, "master"}}},
{scoper, {git, "https://github.com/rbkmoney/scoper", {branch, master}}},
{woody, {git, "https://github.com/rbkmoney/woody_erlang", {branch, master}}}
]}.
%% XRef checks
@ -61,6 +40,7 @@
deprecated_functions_calls,
deprecated_functions
]}.
% at will
% {xref_warnings, true}.
@ -79,10 +59,41 @@
]}.
{plugins, [
{erlfmt, "0.8.0"}
{erlfmt, "0.11.0"},
{rebar3_lint, "0.3.2"}
]}.
{erlfmt, [
{print_width, 120},
{files, "{src,include,test}/*.{hrl,erl}"}
]}.
{elvis, [
#{
dirs => ["src"],
filter => "*.erl",
ruleset => erl_files,
rules => [
{elvis_text_style, line_length, #{limit => 120}}
]
},
#{
dirs => ["test"],
filter => "*.erl",
ruleset => erl_files,
rules => [
{elvis_text_style, line_length, #{limit => 120}},
% Tests are usually more comprehensible when a bit more verbose.
{elvis_style, dont_repeat_yourself, #{min_complexity => 20}}
]
},
#{
dirs => ["."],
filter => "rebar.config",
rules => [
{elvis_text_style, line_length, #{limit => 120}},
{elvis_text_style, no_tabs},
{elvis_text_style, no_trailing_whitespace}
]
}
]}.

View File

@ -1,11 +1,11 @@
{"1.1.0",
{"1.2.0",
[{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},3},
{<<"bouncer_proto">>,
{git,"git@github.com:rbkmoney/bouncer-proto.git",
{git,"https://github.com/rbkmoney/bouncer-proto.git",
{ref,"7ac88717904c6bab73096198b308380e006ed42c"}},
0},
{<<"cache">>,{pkg,<<"cache">>,<<"2.2.0">>},1},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},2},
{<<"cache">>,{pkg,<<"cache">>,<<"2.3.3">>},1},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.3">>},2},
{<<"cg_mon">>,
{git,"https://github.com/rbkmoney/cg_mon.git",
{ref,"5a87a37694e42b6592d3b4164ae54e0e87e24e18"}},
@ -21,52 +21,67 @@
{ref,"7637d915c4c769f7f45c99f8688b17922e801027"}},
0},
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.8.0">>},1},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.2">>},1},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.17.0">>},1},
{<<"how_are_you">>,
{git,"https://github.com/rbkmoney/how_are_you.git",
{ref,"8f11d17eeb6eb74096da7363a9df272fd3099718"}},
1},
{<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},2},
{<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2},
{<<"org_management_proto">>,
{git,"git@github.com:rbkmoney/org-management-proto.git",
{ref,"06c5c8430e445cb7874e54358e457cbb5697fc32"}},
0},
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},3},
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.7.1">>},2},
{<<"scoper">>,
{git,"git@github.com:rbkmoney/scoper.git",
{git,"https://github.com/rbkmoney/scoper.git",
{ref,"89e1af7422199ea3fa287207300bed1d6e00e5ab"}},
0},
{<<"snowflake">>,
{git,"https://github.com/rbkmoney/snowflake.git",
{ref,"7f379ad5e389e1c96389a8d60bae8117965d6a6d"}},
{ref,"de159486ef40cec67074afe71882bdc7f7deab72"}},
1},
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.5">>},2},
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},2},
{<<"thrift">>,
{git,"https://github.com/rbkmoney/thrift_erlang.git",
{ref,"4eda678c985d2894251b91ae43aacf7941846cc9"}},
{ref,"846a0819d9b6d09d0c31f160e33a78dbad2067b4"}},
1},
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},3},
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2},
{<<"woody">>,
{git,"git@github.com:rbkmoney/woody_erlang.git",
{ref,"d106ef66bdd9ac303e05e1d5cddde85e0fa5f36a"}},
{git,"https://github.com/rbkmoney/woody_erlang.git",
{ref,"f2cd30883d58eb1c3ab2172556956f757bc27e23"}},
0}]}.
[
{pkg_hash,[
{<<"bear">>, <<"16264309AE5D005D03718A5C82641FCC259C9E8F09ADEB6FD79CA4271168656F">>},
{<<"cache">>, <<"3C11DBF4CD8FCD5787C95A5FB2A04038E3729CFCA0386016EEA8C953AB48A5AB">>},
{<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>},
{<<"cache">>, <<"B23A5FE7095445A88412A6E614C933377E0137B44FFED77C9B3FEF1A731A20B2">>},
{<<"certifi">>, <<"70BDD7E7188C804F3A30EE0E7C99655BC35D8AC41C23E12325F36AB449B70651">>},
{<<"cowboy">>, <<"91ED100138A764355F43316B1D23D7FF6BDB0DE4EA618CB5D8677C93A7A2F115">>},
{<<"cowlib">>, <<"FD0FF1787DB84AC415B8211573E9A30A3EBE71B5CBFF7F720089972B2319C8A4">>},
{<<"gproc">>, <<"CEA02C578589C61E5341FCE149EA36CCEF236CC2ECAC8691FBA408E7EA77EC2F">>},
{<<"hackney">>, <<"07E33C794F8F8964EE86CEBEC1A8ED88DB5070E52E904B8F12209773C1036085">>},
{<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>},
{<<"hackney">>, <<"717EA195FD2F898D9FE9F1CE0AFCC2621A41ECFE137FAE57E7FE6E9484B9AA99">>},
{<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>},
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
{<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>},
{<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>},
{<<"ranch">>, <<"6B1FAB51B49196860B733A49C07604465A47BDB78AA10C1C16A3D199F7F8C881">>},
{<<"ssl_verify_fun">>, <<"6EAF7AD16CB568BB01753DBBD7A95FF8B91C7979482B95F38443FE2C8852A79B">>},
{<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]}
{<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
{<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]},
{pkg_hash_ext,[
{<<"bear">>, <<"534217DCE6A719D59E54FB0EB7A367900DBFC5F85757E8C1F94269DF383F6D9B">>},
{<<"cache">>, <<"44516CE6FA03594D3A2AF025DD3A87BFE711000EB730219E1DDEFC816E0AA2F4">>},
{<<"certifi">>, <<"ED516ACB3929B101208A9D700062D520F3953DA3B6B918D866106FFA980E1C10">>},
{<<"cowboy">>, <<"04FD8C6A39EDC6AAA9C26123009200FC61F92A3A94F3178C527B70B767C6E605">>},
{<<"cowlib">>, <<"79F954A7021B302186A950A32869DBC185523D99D3E44CE430CD1F3289F41ED4">>},
{<<"gproc">>, <<"580ADAFA56463B75263EF5A5DF4C86AF321F68694E7786CB057FD805D1E2A7DE">>},
{<<"hackney">>, <<"64C22225F1EA8855F584720C0E5B3CD14095703AF1C9FBC845BA042811DC671C">>},
{<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>},
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
{<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>},
{<<"ranch">>, <<"451D8527787DF716D99DC36162FCA05934915DB0B6141BBDAC2EA8D3C7AFC7D7">>},
{<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
{<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]}
].

View File

@ -7,36 +7,47 @@
-define(DEFAULT_DEADLINE, 5000).
%%
-type service_name() :: atom().
-spec call(service_name(), woody:func(), woody:args(), woody_context:ctx()) -> woody:result().
-type service_name() ::
org_management
| bouncer.
-type client_config() :: #{
url := woody:url(),
timeout => non_neg_integer(),
retries => #{woody:func() | '_' => genlib_retry:strategy()}
}.
-type context() :: woody_context:ctx().
-spec call(service_name(), woody:func(), woody:args(), context()) -> woody:result().
call(ServiceName, Function, Args, Context) ->
EventHandler = scoper_woody_event_handler,
call(ServiceName, Function, Args, Context, EventHandler).
-spec call(service_name(), woody:func(), woody:args(), woody_context:ctx(), woody:ev_handler()) -> woody:result().
-spec call(service_name(), woody:func(), woody:args(), context(), woody:ev_handler()) -> woody:result().
call(ServiceName, Function, Args, Context0, EventHandler) ->
Deadline = get_service_deadline(ServiceName),
Context1 = set_deadline(Deadline, Context0),
Retry = get_service_retry(ServiceName, Function),
call(ServiceName, Function, Args, Context1, EventHandler, Retry).
call(ServiceName, Function, Args, Context, EventHandler, Retry) ->
Url = get_service_client_url(ServiceName),
Config = get_service_client_config(ServiceName),
Deadline = get_service_deadline(Config),
Context1 = set_deadline(Deadline, set_default_deadline(Context0)),
Retry = get_service_retry(Function, Config),
Service = get_service_modname(ServiceName),
Request = {Service, Function, Args},
Opts = #{
url => get_service_client_url(Config),
event_handler => EventHandler
},
call_retry(Request, Context1, Opts, Retry).
call_retry(Request, Context, Opts, Retry) ->
try
woody_client:call(
Request,
#{url => Url, event_handler => EventHandler},
Context
)
woody_client:call(Request, Opts, Context)
catch
error:{woody_error, {_Source, Class, _Details}} = Error when
Class =:= resource_unavailable orelse Class =:= result_unknown
->
NextRetry = apply_retry_strategy(Retry, Error, Context),
call(ServiceName, Function, Args, Context, EventHandler, NextRetry)
call_retry(Request, Context, Opts, NextRetry)
end.
apply_retry_strategy(Retry, Error, Context) ->
@ -60,12 +71,13 @@ apply_retry_step({wait, Timeout, Retry}, Deadline0, Error) ->
Retry
end.
-spec get_service_client_config(service_name()) -> client_config().
get_service_client_config(ServiceName) ->
ServiceClients = genlib_app:env(bouncer_client, service_clients, #{}),
maps:get(ServiceName, ServiceClients, #{}).
get_service_client_url(ServiceName) ->
maps:get(url, get_service_client_config(ServiceName), undefined).
get_service_client_url(ClientConfig) ->
maps:get(url, ClientConfig).
-spec get_service_modname(service_name()) -> woody:service().
get_service_modname(org_management) ->
@ -73,22 +85,27 @@ get_service_modname(org_management) ->
get_service_modname(bouncer) ->
{bouncer_decisions_thrift, 'Arbiter'}.
-spec get_service_deadline(service_name()) -> undefined | woody_deadline:deadline().
get_service_deadline(ServiceName) ->
ServiceClient = get_service_client_config(ServiceName),
Timeout = maps:get(deadline, ServiceClient, ?DEFAULT_DEADLINE),
woody_deadline:from_timeout(Timeout).
-spec get_service_deadline(client_config()) -> undefined | woody_deadline:deadline().
get_service_deadline(ClientConfig) ->
case maps:get(timeout, ClientConfig, undefined) of
undefined -> undefined;
Timeout -> woody_deadline:from_timeout(Timeout)
end.
set_deadline(undefined, Context) ->
Context;
set_deadline(Deadline, Context) ->
woody_context:set_deadline(Deadline, Context).
set_default_deadline(Context) ->
case woody_context:get_deadline(Context) of
undefined ->
woody_context:set_deadline(Deadline, Context);
woody_context:set_deadline(woody_deadline:from_timeout(?DEFAULT_DEADLINE), Context);
_AlreadySet ->
Context
end.
get_service_retry(ServiceName, Function) ->
ServiceRetries = genlib_app:env(?APP, service_retries, #{}),
FunctionReties = maps:get(ServiceName, ServiceRetries, #{}),
DefaultRetry = maps:get('_', FunctionReties, finish),
maps:get(Function, FunctionReties, DefaultRetry).
get_service_retry(Function, ClientConfig) ->
FunctionRetries = maps:get(retries, ClientConfig, #{}),
DefaultRetry = maps:get('_', FunctionRetries, finish),
maps:get(Function, FunctionRetries, DefaultRetry).

View File

@ -167,7 +167,7 @@ get_user_orgs_fragment(UserID, WoodyContext) ->
case bouncer_client_woody:call(ServiceName, 'GetUserContext', {UserID}, WoodyContext) of
{ok, EncodedFragment} ->
{ok, {encoded_fragment, convert_fragment(ServiceName, EncodedFragment)}};
{exception, {orgmgmt_UserNotFound}} ->
{exception, {'orgmgmt_UserNotFound'}} ->
{error, {user, notfound}}
end.
@ -180,7 +180,7 @@ get_user_orgs_fragment(UserID, WoodyContext) ->
convert_fragment(
org_management,
{bctx_ContextFragment, Type = v1_thrift_binary, Content}
{'bctx_ContextFragment', Type = v1_thrift_binary, Content}
) when is_binary(Content) ->
#bctx_ContextFragment{
type = Type,

View File

@ -15,6 +15,8 @@
-export([end_per_testcase/2]).
-export([empty_judge/1]).
-export([follows_retries/1]).
-export([follows_timeout/1]).
-export([validate_user_fragment/1]).
-export([validate_env_fragment/1]).
-export([validate_auth_fragment/1]).
@ -40,6 +42,8 @@ groups() ->
[
{default, [], [
empty_judge,
follows_retries,
follows_timeout,
validate_user_fragment,
validate_env_fragment,
validate_auth_fragment,
@ -52,6 +56,11 @@ groups() ->
-type config() :: [{atom(), any()}].
-define(TIMEOUT, 1000).
-define(RETRY_NUM, 3).
-define(RETRY_TIMEOUT, 100).
-define(RETRY_STRATEGY, {linear, ?RETRY_NUM, ?RETRY_TIMEOUT}).
-spec init_per_suite(config()) -> config().
init_per_suite(Config) ->
Apps =
@ -59,8 +68,9 @@ init_per_suite(Config) ->
{service_clients, #{
bouncer => #{
url => <<"http://bouncer:8022/">>,
timeout => ?TIMEOUT,
retries => #{
'Judge' => {linear, 3, 1000},
'Judge' => ?RETRY_STRATEGY,
'_' => finish
}
},
@ -72,7 +82,7 @@ init_per_suite(Config) ->
% default value is 'finish'
% for more info look genlib_retry :: strategy()
% https://github.com/rbkmoney/genlib/blob/master/src/genlib_retry.erl#L19
'GetUserContext' => {linear, 3, 1000},
'GetUserContext' => {linear, 3, 100},
'_' => finish
}
}
@ -111,6 +121,41 @@ empty_judge(C) ->
WoodyContext = woody_context:new(),
allowed = bouncer_client:judge(?RULESET_ID, #{}, WoodyContext).
-spec follows_retries(config()) -> _.
follows_retries(_C) ->
WoodyContext = woody_context:new(),
T0 = erlang:monotonic_time(millisecond),
?assertError(
{woody_error, {internal, resource_unavailable, _}},
bouncer_client:judge(?RULESET_ID, #{}, WoodyContext)
),
T1 = erlang:monotonic_time(millisecond),
?assert(T1 - T0 > ?RETRY_NUM * ?RETRY_TIMEOUT),
?assert(T1 - T0 < ?RETRY_NUM * ?RETRY_TIMEOUT * 1.5).
-spec follows_timeout(config()) -> _.
follows_timeout(C) ->
mock_services(
[
{bouncer, fun('Judge', _) ->
ok = timer:sleep(5000),
{ok, #bdcs_Judgement{
resolution = {allowed, #bdcs_ResolutionAllowed{}}
}}
end}
],
C
),
WoodyContext = woody_context:new(),
T0 = erlang:monotonic_time(millisecond),
?assertError(
{woody_error, {external, result_unknown, _}},
bouncer_client:judge(?RULESET_ID, #{}, WoodyContext)
),
T1 = erlang:monotonic_time(millisecond),
?assert(T1 - T0 > ?TIMEOUT),
?assert(T1 - T0 < ?TIMEOUT * 1.5).
-spec validate_user_fragment(config()) -> _.
validate_user_fragment(C) ->
UserID = <<"somebody">>,
@ -368,7 +413,7 @@ validate_remote_user_fragment(C) ->
id = UserID
}
}),
{ok, {bctx_ContextFragment, v1_thrift_binary, Content}}
{ok, {'bctx_ContextFragment', v1_thrift_binary, Content}}
end},
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
case get_user_id(Fragments) of
@ -475,21 +520,19 @@ set_cfg(Service, Url) ->
mock_services_(Services, Config) when is_list(Config) ->
mock_services_(Services, ?config(test_sup, Config));
mock_services_(Services, SupPid) when is_pid(SupPid) ->
Name = lists:map(fun get_service_name/1, Services),
Port = get_random_port(),
ServerRef = {dummy, lists:map(fun get_service_name/1, Services)},
{ok, IP} = inet:parse_address(?HOST_IP),
ChildSpec = woody_server:child_spec(
{dummy, Name},
#{
ServerRef,
Options = #{
ip => IP,
port => Port,
port => 0,
event_handler => scoper_woody_event_handler,
handlers => lists:map(fun mock_service_handler/1, Services)
}
),
{ok, _} = supervisor:start_child(SupPid, ChildSpec),
{IP, Port} = woody_server:get_addr(ServerRef, Options),
lists:foldl(
fun(Service, Acc) ->
ServiceName = get_service_name(Service),
@ -517,10 +560,6 @@ get_service_modname(org_management) ->
get_service_modname(bouncer) ->
{bouncer_decisions_thrift, 'Arbiter'}.
% TODO not so failproof, ideally we need to bind socket first and then give to a ranch listener
get_random_port() ->
rand:uniform(32768) + 32767.
make_url(ServiceName, Port) ->
iolist_to_binary(["http://", ?HOST_NAME, ":", integer_to_list(Port), make_path(ServiceName)]).