diff --git a/apps/bender/src/bender.erl b/apps/bender/src/bender.erl index 28d8e2e..dd15c40 100644 --- a/apps/bender/src/bender.erl +++ b/apps/bender/src/bender.erl @@ -63,7 +63,7 @@ init([]) -> transport_opts => get_transport_opts(), shutdown_timeout => get_shutdown_timeout(), event_handler => EventHandlers, - handlers => [get_handler_spec()], + handlers => get_handler_spec(), additional_routes => get_routes(EventHandlers) } ), @@ -104,15 +104,24 @@ get_shutdown_timeout() -> genlib_app:env(?MODULE, shutdown_timeout, 0). -spec get_handler_spec() -> - woody:http_handler(woody:th_handler()). + [woody:http_handler(woody:th_handler())]. get_handler_spec() -> - Opts = genlib_app:env(?MODULE, service, #{}), - Path = maps:get(path, Opts, <<"/v1/bender">>), - {Path, { - {bender_thrift, 'Bender'}, - bender_handler - }}. + Opts = genlib_app:env(?MODULE, services, #{}), + Bender = maps:get(bender, Opts, #{}), + Generator = maps:get(generator, Opts, #{}), + BenderPath = maps:get(path, Bender, <<"/v1/bender">>), + GeneratorPath = maps:get(generator_path, Generator, <<"/v1/generator">>), + [ + {BenderPath, { + {bender_thrift, 'Bender'}, + bender_handler + }}, + {GeneratorPath, { + {bender_thrift, 'Generator'}, + generator_handler + }} + ]. -spec get_routes(woody:ev_handlers()) -> [woody_server_thrift_http_handler:route(_)]. diff --git a/apps/bender/src/bender_generator.erl b/apps/bender/src/bender_generator.erl index c5091ae..1088148 100644 --- a/apps/bender/src/bender_generator.erl +++ b/apps/bender/src/bender_generator.erl @@ -3,6 +3,7 @@ %% API -export([bind/4]). +-export([generate/2]). -export([get_internal_id/2]). %% Machinery callbacks diff --git a/apps/bender/src/generator_handler.erl b/apps/bender/src/generator_handler.erl new file mode 100644 index 0000000..44cb0ac --- /dev/null +++ b/apps/bender/src/generator_handler.erl @@ -0,0 +1,66 @@ +-module(generator_handler). + +%% Woody handler + +-behaviour(woody_server_thrift_handler). + +-export([handle_function/4]). + +-include_lib("bender_proto/include/bender_thrift.hrl"). + +-include("bender_internal.hrl"). + +-type woody_context() :: woody_context:ctx(). + +-type schema() :: bender:schema(). +-type generate_id_result() :: bender_thrift:'GeneratedID'(). + +-spec handle_function(woody:func(), woody:args(), woody_context(), woody:options()) -> + {ok, woody:result()}. + +handle_function(Func, Args, WoodyCtx, Opts) -> + scoper:scope(bender, + fun() -> handle_function_(Func, Args, WoodyCtx, Opts) end + ). + +-spec handle_function_(woody:func(), woody:args(), woody_context(), woody:options()) -> + {ok, woody:result()}. + +handle_function_('GenerateID', [Schema], WoodyCtx, _Opts) -> + generate_id(Schema, WoodyCtx). + +-spec generate_id(bender_thrift:'GenerationSchema'(), woody_context()) -> + {ok, generate_id_result()} | no_return(). + +generate_id({constant, #bender_ConstantSchema{} = Schema}, WoodyCtx) -> + NewInternalID = Schema#bender_ConstantSchema.internal_id, + Constant = #constant{internal_id = NewInternalID}, + generate(Constant, WoodyCtx); + +generate_id({sequence, #bender_SequenceSchema{} = Schema}, WoodyCtx) -> + SequenceID = Schema#bender_SequenceSchema.sequence_id, + Minimum = Schema#bender_SequenceSchema.minimum, + Sequence = #sequence{id = SequenceID, minimum = Minimum}, + generate(Sequence, WoodyCtx); + +generate_id({snowflake, #bender_SnowflakeSchema{}}, WoodyCtx) -> + generate(snowflake, WoodyCtx); + +generate_id(Schema, _WoodyCtx) -> + erlang:error({unknown_schema, Schema}). + +-spec generate(schema(), woody_context()) -> + {ok, generate_id_result()} | no_return(). + +generate(Schema, WoodyCtx) -> + case bender_generator:generate(Schema, WoodyCtx) of + {ID, IntegerID} -> + {ok, #bender_GeneratedID{ + id = ID, + integer_id = IntegerID + }}; + ID -> + {ok, #bender_GeneratedID{ + id = ID + }} + end. diff --git a/apps/bender/test/generator_client.erl b/apps/bender/test/generator_client.erl new file mode 100644 index 0000000..73cbde1 --- /dev/null +++ b/apps/bender/test/generator_client.erl @@ -0,0 +1,67 @@ +-module(generator_client). + +-export([new/0]). +-export([generate_id/2]). + +-type client() :: woody_context:ctx(). + +-type schema() :: bender_thrift:'GenerationSchema'(). + +-define(retry_stategy, {linear, 5, 1000}). + +%%% API + +-spec new() -> + client(). + +new() -> + woody_context:new(). + +-spec generate_id(schema(), client()) -> + woody:result() | no_return(). + +generate_id(Schema, Client) -> + call('GenerateID', [Schema], Client). + +%%% Internal functions + +-spec call(atom(), list(), client()) -> + woody:result() | no_return(). + +call(Function, Args, Client) -> + Call = {{bender_thrift, 'Generator'}, Function, Args}, + Opts = #{ + url => <<"http://bender:8022/v1/generator">>, + event_handler => scoper_woody_event_handler, + transport_opts => #{ + max_connections => 10000 + } + }, + call(Call, Opts, Client, ?retry_stategy). + +call(Call, Opts, Client, Retry) -> + try + do_call(Call, Opts, Client) + catch + error:{woody_error, {_Source, Class, _Details}} = Error + when Class =:= resource_unavailable orelse Class =:= result_unknown -> + NextRetry = next_retry(Retry, Error), + call(Call, Opts, Client, NextRetry) + end. + +do_call(Call, Opts, Client) -> + case woody_client:call(Call, Opts, Client) of + {ok, Response} -> + Response; + {exception, Exception} -> + throw(Exception) + end. + +next_retry(Retry, Error) -> + retry_step(genlib_retry:next_step(Retry), Error). + +retry_step(finish, Error) -> + erlang:error(Error); +retry_step({wait, Timeout, Retry}, _) -> + ok = timer:sleep(Timeout), + Retry. diff --git a/apps/bender/test/generator_tests_SUITE.erl b/apps/bender/test/generator_tests_SUITE.erl new file mode 100644 index 0000000..ff92f75 --- /dev/null +++ b/apps/bender/test/generator_tests_SUITE.erl @@ -0,0 +1,161 @@ +-module(generator_tests_SUITE). + +-include_lib("common_test/include/ct.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([constant/1]). +-export([sequence/1]). +-export([sequence_minimum/1]). +-export([snowflake/1]). + + +-include_lib("bender_proto/include/bender_thrift.hrl"). + +-type config() :: [{atom(), term()}]. +-type group_name() :: atom(). +-type test_case_name() :: atom(). + +-define(config(Key, C), (element(2, lists:keyfind(Key, 1, C)))). + +-spec all() -> + [atom()]. + +all() -> + [ + {group, main} + ]. + +-define(parallel_workers, 100). + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. + +groups() -> + [ + {main, [parallel], [ + sequence, + sequence_minimum, + constant, + snowflake + ]} + ]. + +-spec init_per_suite(config()) -> + config(). + +init_per_suite(C) -> + Apps = genlib_app:start_application_with(scoper, [ + {storage, scoper_storage_logger} + ]) ++ genlib_app:start_application_with(bender, [ + {sequence, #{ + path => <<"/v1/stateproc/bender_sequence">>, + schema => machinery_mg_schema_generic, + url => <<"http://machinegun:8022/v1/automaton">>, + event_handler => scoper_woody_event_handler, + transport_opts => #{ + max_connections => 1000 + } + }}, + {protocol_opts, #{ + timeout => 60000 + }}, + {transport_opts, #{ + max_connections => 10000, + num_acceptors => 100 + }} + ]), + [{suite_apps, Apps} | C]. + +-spec end_per_suite(config()) -> + ok. + +end_per_suite(C) -> + genlib_app:stop_unload_applications(?config(suite_apps, C)). + +-spec init_per_testcase(atom(), config()) -> + config(). + +init_per_testcase(_Name, C) -> + Client = generator_client:new(), + [{client, Client} | C]. + +-spec end_per_testcase(atom(), config()) -> + config(). + +end_per_testcase(_Name, _C) -> + ok. + +-spec constant(config()) -> + ok. + +constant(C) -> + Client = get_client(C), + InternalID = bender_utils:unique_id(), + Schema = {constant, #bender_ConstantSchema{internal_id = InternalID}}, + InternalID = generate(Schema, Client), + ok. + +-spec sequence(config()) -> + ok. + +sequence(C) -> + Client = get_client(C), + SequenceID = bender_utils:unique_id(), + ExpectedIDs = lists:seq(1, ?parallel_workers), + GeneratedIDs = genlib_pmap:map( + fun(_) -> + Schema = {sequence, #bender_SequenceSchema{sequence_id = SequenceID}}, + {_, IntegerID} = generate(Schema, Client), + IntegerID + end, + ExpectedIDs + ), + ExpectedIDs = lists:sort(GeneratedIDs), + ok. + +-spec sequence_minimum(config()) -> + ok. + +sequence_minimum(C) -> + Client = get_client(C), + SequenceID = bender_utils:unique_id(), + Schema1 = {sequence, #bender_SequenceSchema{sequence_id = SequenceID}}, + {<<"1">>, 1} = generate(Schema1, Client), + Schema2 = {sequence, #bender_SequenceSchema{sequence_id = SequenceID, minimum = 4}}, + {<<"4">>, 4} = generate(Schema2, Client), + OtherSeqID = bender_utils:unique_id(), + Schema3 = {sequence, #bender_SequenceSchema{sequence_id = OtherSeqID, minimum = 8}}, + {<<"8">>, 8} = generate(Schema3, Client), + ok. + +-spec snowflake(config()) -> + ok. + +snowflake(C) -> + Client = get_client(C), + Schema = {snowflake, #bender_SnowflakeSchema{}}, + {_ID, _IntegerID} = generate(Schema, Client), + ok. + + +%%% + +get_client(C) -> + ?config(client, C). + +generate(Schema, Client) -> + case generator_client:generate_id(Schema, Client) of + #bender_GeneratedID{ + id = ID, + integer_id = undefined + } -> ID; + #bender_GeneratedID{ + id = ID, + integer_id = IntegerID + } -> {ID, IntegerID} + end. diff --git a/config/sys.config b/config/sys.config index fe88812..9d5c35f 100644 --- a/config/sys.config +++ b/config/sys.config @@ -1,7 +1,8 @@ [ {bender, [ - {service, #{ - path => <<"/v1/bender">> + {services, #{ + bender => #{path => <<"/v1/bender">>}, + generator => #{path => <<"/v1/generator">>} }}, {generator, #{ diff --git a/rebar.config b/rebar.config index 7233d1c..1d72c4a 100644 --- a/rebar.config +++ b/rebar.config @@ -141,4 +141,4 @@ rebar3_run ]}. -%%{ct_readable, true}. \ No newline at end of file +%%{ct_readable, true}. diff --git a/rebar.lock b/rebar.lock index d93c74c..ad572f6 100644 --- a/rebar.lock +++ b/rebar.lock @@ -2,7 +2,7 @@ [{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},2}, {<<"bender_proto">>, {git,"git@github.com:rbkmoney/bender-proto.git", - {ref,"df5bedd950dd2492e1760eceeb9207645b9af822"}}, + {ref,"0d5813b8a25c8d03e4e59e42aa5f4e9b785a3849"}}, 0}, {<<"cache">>,{pkg,<<"cache">>,<<"2.2.0">>},1}, {<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},2}, @@ -22,7 +22,7 @@ 1}, {<<"genlib">>, {git,"https://github.com/rbkmoney/genlib.git", - {ref,"941b8c8d4dc544b740a2429ea8381c3873e5fe75"}}, + {ref,"1ca08793ad8af0beb26eda8cd00687c69f7ef8b4"}}, 0}, {<<"gproc">>,{pkg,<<"gproc">>,<<"0.8.0">>},1}, {<<"hackney">>,{pkg,<<"hackney">>,<<"1.16.0">>},1},