Introduce machinegun client/processor handler lib (#1)

This commit is contained in:
Anton Belyaev 2018-05-30 17:45:42 +03:00 committed by GitHub
parent 0d9dc7c155
commit 7dac1c80f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1984 additions and 2 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# general
log
/_build/
/_checkouts/
*~
erl_crash.dump
.tags*
*.sublime-workspace
.DS_Store
docker-compose.yml
/.idea/
*.beam
/.edts

53
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,53 @@
#!groovy
// -*- mode: groovy -*-
def finalHook = {
runStage('store CT logs') {
archive '_build/test/logs/'
}
}
build('machinery', 'docker-host', finalHook) {
checkoutRepo()
loadBuildUtils()
def pipeDefault
def withWsCache
runStage('load pipeline') {
env.JENKINS_LIB = "build_utils/jenkins_lib"
pipeDefault = load("${env.JENKINS_LIB}/pipeDefault.groovy")
withWsCache = load("${env.JENKINS_LIB}/withWsCache.groovy")
}
pipeDefault() {
if (!masterlikeBranch()) {
runStage('compile') {
withGithubPrivkey {
sh 'make wc_compile'
}
}
runStage('lint') {
sh 'make wc_lint'
}
runStage('xref') {
sh 'make wc_xref'
}
runStage('dialyze') {
withWsCache("_build/default/rebar3_19.1_plt") {
sh 'make wc_dialyze'
}
}
runStage('test') {
sh "make wdeps_test"
}
}
}
}

48
Makefile Normal file
View File

@ -0,0 +1,48 @@
REBAR := $(shell which rebar3 2>/dev/null || which ./rebar3)
UTILS_PATH := build_utils
TEMPLATES_PATH := .
SUBMODULES = $(UTILS_PATH)
SUBTARGETS = $(patsubst %,%/.git,$(SUBMODULES))
SERVICE_NAME := machinery
BUILD_IMAGE_TAG := eee42f2ca018c313190bc350fe47d4dea70b6d27
CALL_ANYWHERE := all submodules compile xref lint dialyze clean distclean
CALL_W_CONTAINER := $(CALL_ANYWHERE) test
all: compile
-include $(UTILS_PATH)/make_lib/utils_container.mk
.PHONY: $(CALL_W_CONTAINER)
$(SUBTARGETS): %/.git: %
git submodule update --init $<
touch $@
submodules: $(SUBTARGETS)
compile: submodules
$(REBAR) compile
xref: submodules
$(REBAR) xref
lint:
elvis rock
dialyze: submodules
$(REBAR) dialyzer
clean:
$(REBAR) clean
distclean:
$(REBAR) clean -a
rm -rf _build
test: submodules
$(REBAR) ct

View File

@ -1,3 +1,3 @@
# Machinegun client
# Machinery
Client for [machinegun](https://github.com/rbkmoney/machinegun).
Client/hander lib for [machinegun](https://github.com/rbkmoney/machinegun).

28
docker-compose.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
cat <<EOF
version: '2.1'
services:
${SERVICE_NAME}:
image: ${BUILD_IMAGE}
volumes:
- .:$PWD
- $HOME/.cache:/home/$UNAME/.cache
working_dir: $PWD
command: /sbin/init
depends_on:
machinegun:
condition: service_healthy
machinegun:
image: dr.rbkmoney.com/rbkmoney/machinegun:5756aa3070f9beebd4b20d7076c8cdc079286090
command: /opt/machinegun/bin/machinegun foreground
volumes:
- ./test/machinegun/config.yaml:/opt/machinegun/etc/config.yaml
healthcheck:
test: "curl http://localhost:8022/"
interval: 5s
timeout: 1s
retries: 20
EOF

77
elvis.config Normal file
View File

@ -0,0 +1,77 @@
[
{elvis, [
{config, [
#{
dirs => ["src"],
filter => "*.erl",
ignore => ["_thrift.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 => 3}},
{elvis_style, god_modules, #{limit => 30, ignore => [hg_client_party]}},
{elvis_style, no_if_expression},
{elvis_style, invalid_dynamic_call, #{ignore => [elvis]}},
{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 => 15}},
{elvis_style, no_debug_call, #{ignore => [elvis, elvis_utils]}}
]
},
#{
dirs => ["test"],
filter => "*.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 => 3}},
{elvis_style, no_if_expression},
{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, no_spec_with_records},
{elvis_style, dont_repeat_yourself, #{min_complexity => 30}}
]
},
#{
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}
]
},
#{
dirs => ["src"],
filter => "*.app.src",
rules => [
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_style, no_tabs},
{elvis_style, no_trailing_whitespace}
]
}
]}
]}
].

71
rebar.config Normal file
View File

@ -0,0 +1,71 @@
% Common project erlang options.
{erl_opts, [
% mandatory
debug_info,
warnings_as_errors,
warn_export_all,
warn_missing_spec,
warn_untyped_record,
warn_export_vars,
% by default
warn_unused_record,
warn_bif_clash,
warn_obsolete_guard,
warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_unused_function,
warn_deprecated_function
% at will
% bin_opt_info
% no_auto_import
% warn_missing_spec_all
]}.
% Common project dependencies.
{deps, [
{genlib,
{git, "https://github.com/rbkmoney/genlib.git", {branch, "master"}}
},
{rfc3339,
"0.2.2"
},
{woody,
{git, "git@github.com:rbkmoney/woody_erlang.git", {branch, "master"}}
},
{mg_proto,
{git, "git@github.com:rbkmoney/machinegun_proto.git", {branch, "master"}}
}
]}.
{xref_checks, [
undefined_function_calls,
undefined_functions,
deprecated_functions_calls,
deprecated_functions
]}.
{dialyzer, [
{warnings, [
% mandatory
unmatched_returns,
error_handling,
race_conditions,
unknown
]},
{plt_apps, all_deps}
]}.
{profiles, [
{test, [
{deps, [
{lager,
"3.6.1"
}
]}
]}
]}.

44
rebar.lock Normal file
View File

@ -0,0 +1,44 @@
{"1.1.0",
[{<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},2},
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"1.0.4">>},1},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},2},
{<<"genlib">>,
{git,"https://github.com/rbkmoney/genlib.git",
{ref,"e9e5aed04a870a064312590e798f89d46ce5585c"}},
0},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.6.2">>},1},
{<<"idna">>,{pkg,<<"idna">>,<<"1.2.0">>},2},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
{<<"mg_proto">>,
{git,"git@github.com:rbkmoney/machinegun_proto.git",
{ref,"5c07c579014f9900357f7a72f9d10a03008b9da1"}},
0},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.0.2">>},2},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},2},
{<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.2.2">>},0},
{<<"snowflake">>,
{git,"https://github.com/rbkmoney/snowflake.git",
{ref,"0a598108f6582affe3b4ae550fc5b9f2062e318a"}},
1},
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.1">>},2},
{<<"thrift">>,
{git,"https://github.com/rbkmoney/thrift_erlang.git",
{ref,"240bbc842f6e9b90d01bd07838778cf48752b510"}},
1},
{<<"woody">>,
{git,"git@github.com:rbkmoney/woody_erlang.git",
{ref,"06ef3d63c0b6777e7cfa4b4f949eb34008291c0e"}},
0}]}.
[
{pkg_hash,[
{<<"certifi">>, <<"861A57F3808F7EB0C2D1802AFEAAE0FA5DE813B0DF0979153CBAFCD853ABABAF">>},
{<<"cowboy">>, <<"A324A8DF9F2316C833A470D918AAF73AE894278B8AA6226CE7A9BF699388F878">>},
{<<"cowlib">>, <<"9D769A1D062C9C3AC753096F868CA121E2730B9A377DE23DEC0F7E08B1DF84EE">>},
{<<"hackney">>, <<"96A0A5E7E65B7ACAD8031D231965718CC70A9B4131A8B033B7543BBD673B8210">>},
{<<"idna">>, <<"AC62EE99DA068F43C50DC69ACF700E03A62A348360126260E87F2B54ECED86B2">>},
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
{<<"mimerl">>, <<"993F9B0E084083405ED8252B99460C4F0563E41729AB42D9074FD5E52439BE88">>},
{<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>},
{<<"rfc3339">>, <<"1552DF616ACA368D982E9F085A0E933B6688A3F4938A671798978EC2C0C58730">>},
{<<"ssl_verify_fun">>, <<"28A4D65B7F59893BC2C7DE786DEC1E1555BD742D336043FE644AE956C3497FBE">>}]}
].

23
src/machinery.app.src Normal file
View File

@ -0,0 +1,23 @@
{application, machinery, [
{description,
"Machinegun client/handler lib."
},
{vsn, "1"},
{registered, []},
{applications, [
kernel,
stdlib,
genlib,
cowboy,
woody,
mg_proto
]},
{env, []},
{modules, []},
{maintainers, [
"Anton Belyaev <a.belyaev@rbkmoney.com>",
"Andrey Mayorov <a.mayorov@rbkmoney.com>"
]},
{licenses, []},
{links, ["https://github.com/rbkmoney/machinery"]}
]}.

150
src/machinery.erl Normal file
View File

@ -0,0 +1,150 @@
%%%
%%% Machine API abstraction.
%%% Behaviour and API.
%%%
%% TODO
%%
%% - What if we make `start` idempotent and argsless in the `machinery`.
%% - Storage schema behaviour
-module(machinery).
-type namespace() :: atom().
-type id() :: binary().
-type args(T) :: T.
-type response(T) :: T.
-type usec_part() :: 0..999999.
-type timestamp() :: {calendar:datetime(), usec_part()}.
-type event_id() :: integer().
-type event_body(T) :: T.
-type event(T) :: {event_id(), timestamp(), event_body(T)}.
-type history(T) :: [event(T)].
-type aux_state(T) :: T.
-type event_cursor() :: undefined | event_id().
-type limit() :: undefined | pos_integer().
-type direction() :: forward | backward.
-type scope() :: {event_cursor(), limit(), direction()}.
-type signal(T) :: {init, args(T)} | timeout.
-type machine(T) :: #{
namespace := namespace(),
id := id(),
history := history(T),
aux_state := aux_state(_)
%% TODO
%% history_range ?
%% timer ?
}.
-export_type([namespace/0]).
-export_type([id/0]).
-export_type([scope/0]).
-export_type([args/1]).
-export_type([response/1]).
-export_type([machine/1]).
-type modopts(O) :: module() | {module(), O}.
%% handler
-type handler_opts(T) :: T. %% provided to logic handler from machinery backend
-type handler_args(T) :: args(T). %% provided to logic handler from handler server spec
-type logic_handler(A) :: modopts(handler_args(A)).
%% client
-type backend_opts(T) :: T. %% opts for client backend
-type backend(O) :: modopts(backend_opts(O)). %% client backend
-export_type([modopts/1]).
-export_type([handler_opts/1]).
-export_type([handler_args/1]).
-export_type([logic_handler/1]).
-export_type([backend_opts/1]).
-export_type([backend/1]).
%% API
-export([start/4]).
-export([call/4]).
-export([call/5]).
-export([get/3]).
-export([get/4]).
%% Internal API
-export([dispatch_signal/4]).
-export([dispatch_call/4]).
%% Behaviour definition
-type seconds() :: pos_integer().
-type timer() ::
{timeout, seconds()} |
{deadline, timestamp()}.
-type result(T) :: #{
events => [event_body(T)],
action => action() | [action()],
aux_state => aux_state(_)
}.
-type action() ::
{set_timer, timer()} |
unset_timer |
continue |
remove.
-export_type([result/1]).
-export_type([action/0]).
-callback init(args(_), machine(T), handler_args(_), handler_opts(_)) ->
result(T).
-callback process_timeout(machine(T), handler_args(_), handler_opts(_)) ->
result(T).
-callback process_call(args(_), machine(T), handler_args(_), handler_opts(_)) ->
{response(_), result(T)}.
%% API
-spec start(namespace(), id(), args(_), backend(_)) ->
ok | {error, exists}.
start(NS, ID, Args, Backend) ->
{Module, Opts} = machinery_utils:get_backend(Backend),
machinery_backend:start(Module, NS, ID, Args, Opts).
-spec call(namespace(), id(), args(_), backend(_)) ->
{ok, response(_)} | {error, notfound}.
call(NS, ID, Args, Backend) ->
call(NS, ID, {undefined, undefined, forward}, Args, Backend).
-spec call(namespace(), id(), scope(), args(_), backend(_)) ->
{ok, response(_)} | {error, notfound}.
call(NS, ID, Scope, Args, Backend) ->
{Module, Opts} = machinery_utils:get_backend(Backend),
machinery_backend:call(Module, NS, ID, Scope, Args, Opts).
-spec get(namespace(), id(), backend(_)) ->
{ok, machine(_)} | {error, notfound}.
get(NS, ID, Backend) ->
get(NS, ID, {undefined, undefined, forward}, Backend).
-spec get(namespace(), id(), scope(), backend(_)) ->
{ok, machine(_)} | {error, notfound}.
get(NS, ID, Scope, Backend) ->
{Module, Opts} = machinery_utils:get_backend(Backend),
machinery_backend:get(Module, NS, ID, Scope, Opts).
%% Internal API
-spec dispatch_signal(signal(_), machine(T), logic_handler(_), handler_opts(_)) ->
result(T).
dispatch_signal({init, Args}, Machine, {Handler, HandlerArgs}, Opts) ->
Handler:init(Args, Machine, HandlerArgs, Opts);
dispatch_signal(timeout, Machine, {Handler, HandlerArgs}, Opts) ->
Handler:process_timeout(Machine, HandlerArgs, Opts).
-spec dispatch_call(args(_), machine(T), logic_handler(_), handler_opts(_)) ->
{response(_), result(T)}.
dispatch_call(Args, Machine, {Handler, HandlerArgs}, Opts) ->
Handler:process_call(Args, Machine, HandlerArgs, Opts).

45
src/machinery_backend.erl Normal file
View File

@ -0,0 +1,45 @@
%%%
%%% Machinery backend behaviour.
-module(machinery_backend).
%% API
-export([start/5]).
-export([call/6]).
-export([get/5]).
%% Behaviour definition
-type namespace() :: machinery:namespace().
-type id() :: machinery:id().
-type scope() :: machinery:scope().
-type args() :: machinery:args(_).
-type backend_opts() :: machinery:backend_opts(_).
-callback start(namespace(), id(), args(), backend_opts()) ->
ok | {error, exists}.
-callback call(namespace(), id(), scope(), args(), backend_opts()) ->
{ok, machinery:response(_)} | {error, notfound}.
-callback get(namespace(), id(), scope(), backend_opts()) ->
{ok, machinery:machine(_)} | {error, notfound}.
%% API
-type backend() :: module().
-spec start(backend(), namespace(), id(), args(), backend_opts()) ->
ok | {error, exists}.
start(Backend, Namespace, Id, Args, Opts) ->
Backend:start(Namespace, Id, Args, Opts).
-spec call(backend(), namespace(), id(), scope(), args(), backend_opts()) ->
{ok, machinery:response(_)} | {error, notfound}.
call(Backend, Namespace, Id, Scope, Args, Opts) ->
Backend:call(Namespace, Id, Scope, Args, Opts).
-spec get(backend(), namespace(), id(), scope(), backend_opts()) ->
{ok, machinery:machine(_)} | {error, notfound}.
get(Backend, Namespace, Id, Scope, Opts) ->
Backend:get(Namespace, Id, Scope, Opts).

View File

@ -0,0 +1,120 @@
%%%
%%% Machine behaving as a unique tag.
-module(machinery_machine_unique_tag).
%% API
-type id() :: machinery:id().
-type namespace() :: machinery:namespace().
-type tag() :: binary().
-export_type([tag/0]).
-export([tag/4]).
-export([untag/4]).
-export([get/3]).
%% Machine behaviour
-behaviour(machinery).
-export([init/4]).
-export([process_timeout/3]).
-export([process_call/4]).
%%
-spec tag(namespace(), tag(), id(), machinery:backend(_)) ->
ok | {error, {set, id()}}.
tag(NS, Tag, ID, Backend) ->
case machinery:start(construct_namespace(NS), Tag, {tag, ID}, Backend) of
ok ->
ok;
{error, exists} ->
case get(NS, Tag, Backend) of
{ok, ID} ->
ok;
{ok, IDWas} ->
{error, {set, IDWas}};
{error, unset} ->
tag(NS, Tag, ID, Backend)
end
end.
-spec untag(namespace(), tag(), id(), machinery:backend(_)) ->
ok | {error, {set, id()}}.
untag(NS, Tag, ID, Backend) ->
case machinery:call(construct_namespace(NS), Tag, {untag, ID}, Backend) of
{ok, ok} ->
ok;
{ok, {error, IDWas}} ->
{error, {set, IDWas}};
{error, notfound} ->
ok
end.
-spec get(namespace(), tag(), machinery:backend(_)) ->
{ok, id()} | {error, unset}.
get(NS, Tag, Backend) ->
case machinery:get(construct_namespace(NS), Tag, Backend) of
{ok, Machine} ->
ID = get_machine_st(Machine),
{ok, ID};
{error, notfound} ->
{error, unset}
end.
construct_namespace(NS) ->
list_to_atom(atom_to_list(NS) ++ "/tags").
%%
-type machine() :: machinery:machine(ev()).
-type handler_opts() :: machinery:handler_opts(_).
-type result() :: machinery:result(ev()).
-type response() :: machinery:response(
ok | {error, id()}
).
-type ev() ::
{tag_set, id()} |
tag_unset.
-spec init({tag, id()}, machine(), undefined, handler_opts()) ->
result().
init({tag, ID}, _Machine, _, _Opts) ->
#{
events => [ID]
}.
-spec process_timeout(machine(), undefined, handler_opts()) ->
result().
process_timeout(#{}, _, _Opts) ->
#{}.
-spec process_call({untag, id()}, machine(), undefined, handler_opts()) ->
{response(), result()}.
process_call({untag, ID}, Machine, _, _Opts) ->
case get_machine_st(Machine) of
ID ->
{ok, #{
action => [remove]
}};
IDWas ->
{{error, IDWas}, #{}}
end.
%%
get_machine_st(#{history := History}) ->
collapse_history(History).
collapse_history(History) ->
lists:foldl(fun apply_event/2, undefined, History).
apply_event({_ID, _CreatedAt, Body}, St) ->
apply_event_body(Body, St).
apply_event_body(ID, undefined) ->
ID.

View File

@ -0,0 +1,96 @@
-module(machinery_machine_unique_tag_mg_example).
%% API
-export([child_spec/1]).
-export([tag/4]).
-export([untag/4]).
-export([get/3]).
-type id() :: machinery:id().
-type namespace() :: machinery:namespace().
-type tag() :: machinery_machine_unique_tag:tag().
-type opts() :: #{
woody_ctx := woody_context:ctx()
}.
-export_type([opts/0]).
-define(LOGIC_HANDLER, machinery_machine_unique_tag).
%% API
-spec child_spec(_Id) ->
supervisor:child_spec().
child_spec(Id) ->
machinery_utils:woody_child_spec(Id, get_routes(), config(server_opts)).
-spec tag(namespace(), tag(), id(), opts()) ->
ok | {error, {set, id()}}.
tag(NS, Tag, ID, Opts) ->
machinery_machine_unique_tag:tag(NS, Tag, ID, get_backend(Opts)).
-spec untag(namespace(), tag(), id(), opts()) ->
ok | {error, {set, id()}}.
untag(NS, Tag, ID, Opts) ->
machinery_machine_unique_tag:untag(NS, Tag, ID, get_backend(Opts)).
-spec get(namespace(), tag(), opts()) ->
{ok, id()} | {error, unset}.
get(NS, Tag, Opts) ->
machinery_machine_unique_tag:get(NS, Tag, get_backend(Opts)).
%%% Internal functions
%% handler
-spec get_routes() ->
machinery_utils:woody_routes().
get_routes() ->
machinery_mg_backend:get_routes(
[get_handler(config(handler_path))],
maps:with([event_handler, handler_limits], config())
).
get_handler(Path) ->
{?LOGIC_HANDLER, get_handler_config(Path)}.
get_handler_config(Path) ->
#{path => Path, backend_config => get_backend_config()}.
get_backend_config() ->
maps:with([schema], config()).
%% client
-spec get_backend(opts()) ->
machinery_mg_backend:backend().
get_backend(Opts) ->
machinery_mg_backend:new(get_backend_opts(Opts)).
-spec get_backend_opts(opts()) ->
machinery_mg_backend:backend_opts().
get_backend_opts(#{woody_ctx := WoodyCtx}) ->
#{
woody_ctx => WoodyCtx,
client => get_woody_client(),
schema => config(schema)
}.
-spec get_woody_client() ->
machinery_mg_client:woody_client().
get_woody_client() ->
maps:with([url, event_handler], config()).
%% config
config(Key) ->
maps:get(Key, config()).
config() ->
#{
schema => machinery_mg_schema_generic,
url => <<"http://machinegun:8022/v1/automaton">>,
handler_path => <<"/v1/stateproc">>,
event_handler => woody_event_handler_default,
server_opts => #{
ip => {0, 0, 0, 0},
port => 8022
}
}.

View File

@ -0,0 +1,497 @@
%%%
%%% Machinery machinegun backend
%% TODO
%%
%% - There's marshalling scattered around which is common enough for _any_ thrift interface.
-module(machinery_mg_backend).
-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl").
-type namespace() :: machinery:namespace().
-type id() :: machinery:id().
-type scope() :: machinery:scope().
-type args(T) :: machinery:args(T).
-type response(T) :: machinery:response(T).
-type machine(T) :: machinery:machine(T).
-type logic_handler(T) :: machinery:logic_handler(T).
-define(BACKEND_CORE_OPTS,
schema := machinery_mg_schema:schema()
).
%% Server types
-type backend_config() :: #{
?BACKEND_CORE_OPTS
}.
-type handler_config() :: #{
path := woody:path(),
backend_config := backend_config()
}.
-type handler(A) :: {logic_handler(A), handler_config()}. %% handler server spec
-type handler_opts() :: machinery:handler_opts(#{
woody_ctx := woody_context:ctx()
}).
-type backend_handler_opts() :: #{
handler := logic_handler(_),
?BACKEND_CORE_OPTS
}.
%% Client types
-type backend_opts() :: machinery:backend_opts(#{
woody_ctx := woody_context:ctx(),
client := machinery_mg_client:woody_client(),
?BACKEND_CORE_OPTS
}).
-type backend() :: {?MODULE, backend_opts()}.
-export_type([backend_config/0]).
-export_type([handler_config/0]).
-export_type([logic_handler/1]).
-export_type([handler/1]).
-export_type([handler_opts/0]).
-export_type([backend_opts/0]).
-export_type([backend/0]).
%% API
-export([get_routes/2]).
-export([get_handler/2]).
-export([new/1]).
%% Machinery backend
-behaviour(machinery_backend).
-export([start/4]).
-export([call/5]).
-export([get/4]).
%% Woody handler
-behaviour(woody_server_thrift_handler).
-export([handle_function/4]).
%% API
-spec get_routes([handler(_)], machinery_utils:route_opts()) ->
machinery_utils:woody_routes().
get_routes(Handlers, Opts) ->
machinery_utils:get_woody_routes(Handlers, fun get_handler/2, Opts).
-spec get_handler(handler(_), machinery_utils:route_opts()) ->
machinery_utils:woody_handler().
get_handler({LogicHandler, #{path := Path, backend_config := Config}}, _) ->
{Path, {
{mg_proto_state_processing_thrift, 'Processor'},
{?MODULE, get_backend_handler_opts(LogicHandler, Config)}
}}.
-spec new(backend_opts()) ->
backend().
new(Opts = #{woody_ctx := _, client := _, schema := _}) ->
{?MODULE, Opts}.
%% Machinery backend
-spec start(namespace(), id(), args(_), backend_opts()) ->
ok | {error, exists}.
start(NS, ID, Args, Opts) ->
Client = get_client(Opts),
Schema = get_schema(Opts),
InitArgs = marshal({schema, Schema, {args, init}}, Args),
case machinery_mg_client:start(marshal(namespace, NS), marshal(id, ID), InitArgs, Client) of
{ok, ok} ->
ok;
{exception, #mg_stateproc_MachineAlreadyExists{}} ->
{error, exists};
{exception, #mg_stateproc_NamespaceNotFound{}} ->
error({namespace_not_found, NS});
{exception, #mg_stateproc_MachineFailed{}} ->
error({failed, NS, ID})
end.
-spec call(namespace(), id(), scope(), args(_), backend_opts()) ->
{ok, response(_)} | {error, notfound}.
call(NS, ID, Scope, Args, Opts) ->
Client = get_client(Opts),
Schema = get_schema(Opts),
Descriptor = {NS, ID, Scope},
CallArgs = marshal({schema, Schema, {args, call}}, Args),
case machinery_mg_client:call(marshal(descriptor, Descriptor), CallArgs, Client) of
{ok, Response} ->
{ok, unmarshal({schema, Schema, response}, Response)};
{exception, #mg_stateproc_MachineNotFound{}} ->
{error, notfound};
{exception, #mg_stateproc_NamespaceNotFound{}} ->
error({namespace_not_found, NS});
{exception, #mg_stateproc_MachineFailed{}} ->
error({failed, NS, ID})
end.
-spec get(namespace(), id(), scope(), backend_opts()) ->
{ok, machine(_)} | {error, notfound}.
get(NS, ID, Scope, Opts) ->
Client = get_client(Opts),
Schema = get_schema(Opts),
Descriptor = {NS, ID, Scope},
case machinery_mg_client:get_machine(marshal(descriptor, Descriptor), Client) of
{ok, Machine} ->
{ok, unmarshal({machine, Schema}, Machine)};
{exception, #mg_stateproc_MachineNotFound{}} ->
{error, notfound};
{exception, #mg_stateproc_NamespaceNotFound{}} ->
error({namespace_not_found, NS})
end.
%% Woody handler
-spec handle_function
('ProcessSignal', woody:args(), woody_context:ctx(), backend_handler_opts()) ->
{ok, mg_proto_state_processing_thrift:'SignalResult'()};
('ProcessCall', woody:args(), woody_context:ctx(), backend_handler_opts()) ->
{ok, mg_proto_state_processing_thrift:'CallResult'()}.
handle_function(
'ProcessSignal',
[#mg_stateproc_SignalArgs{signal = Signal, machine = Machine}],
WoodyCtx,
#{handler := Handler, schema := Schema}
) ->
Machine1 = unmarshal({machine, Schema}, Machine),
Result = dispatch_signal(
unmarshal({signal, Schema}, Signal),
Machine1,
machinery_utils:get_handler(Handler),
get_handler_opts(WoodyCtx)
),
{ok, marshal({signal_result, Schema}, handle_result(Result, Machine1))};
handle_function(
'ProcessCall',
[#mg_stateproc_CallArgs{arg = Args, machine = Machine}],
WoodyCtx,
#{handler := Handler, schema := Schema}
) ->
Machine1 = unmarshal({machine, Schema}, Machine),
{Response, Result} = dispatch_call(
unmarshal({schema, Schema, {args, call}}, Args),
Machine1,
machinery_utils:get_handler(Handler),
get_handler_opts(WoodyCtx)
),
{ok, marshal({call_result, Schema}, {Response, handle_result(Result, Machine1)})}.
%% Utils
-spec get_backend_handler_opts(logic_handler(_), backend_config()) ->
backend_handler_opts().
get_backend_handler_opts(Handler, Config) ->
Config#{handler => Handler}.
get_schema(#{schema := Schema}) ->
Schema.
get_client(#{client := Client, woody_ctx := WoodyCtx}) ->
machinery_mg_client:new(Client, WoodyCtx).
get_handler_opts(WoodyCtx) ->
#{woody_ctx => WoodyCtx}.
dispatch_signal(Signal, Machine, Handler, Opts) ->
machinery:dispatch_signal(Signal, Machine, Handler, Opts).
dispatch_call(Args, Machine, Handler, Opts) ->
machinery:dispatch_call(Args, Machine, Handler, Opts).
handle_result(Result, OrigMachine) ->
Result#{aux_state => set_aux_state(
maps:get(aux_state, Result, undefined),
maps:get(aux_state, OrigMachine, machinery_msgpack:nil())
)}.
set_aux_state(undefined, ReceivedState) ->
ReceivedState;
set_aux_state(NewState, _) ->
NewState.
%% Marshalling
%% No marshalling for the machine required by the protocol so far.
%%
%% marshal(
%% {machine, Schema},
%% #{
%% ns := NS,
%% id := ID,
%% history := History
%% }
%% ) ->
%% #mg_stateproc_Machine{
%% 'ns' = marshal(namespace, NS),
%% 'id' = marshal(id, ID),
%% 'history' = marshal({history, Schema}, History),
%% % TODO
%% % There are required fields left
%% 'history_range' = marshal(range, {undefined, undefined, forward})
%% };
marshal(descriptor, {NS, ID, Scope}) ->
#mg_stateproc_MachineDescriptor{
'ns' = marshal(namespace, NS),
'ref' = {'id', marshal(id, ID)},
'range' = marshal(range, Scope)
};
marshal(range, {Cursor, Limit, Direction}) ->
#mg_stateproc_HistoryRange{
'after' = marshal({maybe, event_id}, Cursor),
'limit' = marshal(limit, Limit),
'direction' = marshal(direction, Direction)
};
marshal({history, Schema}, V) ->
marshal({list, {event, Schema}}, V);
marshal({event, Schema}, {EventID, CreatedAt, Body}) ->
#mg_stateproc_Event{
'id' = marshal(event_id, EventID),
'created_at' = marshal(timestamp, CreatedAt),
'event_payload' = marshal({schema, Schema, event}, Body)
};
marshal({signal, Schema}, {init, Args}) ->
{init, #mg_stateproc_InitSignal{arg = marshal({schema, Schema, {args, init}}, Args)}};
marshal({signal, _Schema}, timeout) ->
{timeout, #mg_stateproc_TimeoutSignal{}};
marshal({signal, Schema}, {repair, Args}) ->
{repair, #mg_stateproc_RepairSignal{arg = marshal({maybe, {schema, Schema, {args, repair}}}, Args)}};
marshal({signal_result, Schema}, #{} = V) ->
#mg_stateproc_SignalResult{
change = marshal({state_change, Schema}, V),
action = marshal(action, maps:get(action, V, []))
};
marshal({call_result, Schema}, {Response, #{} = V}) ->
#mg_stateproc_CallResult{
response = marshal({schema, Schema, response}, Response),
change = marshal({state_change, Schema}, V),
action = marshal(action, maps:get(action, V, []))
};
marshal({state_change, Schema}, #{} = V) ->
#mg_stateproc_MachineStateChange{
events = marshal({list, {schema, Schema, event}}, maps:get(events, V, [])),
% TODO
% Provide this to logic handlers as well
aux_state = marshal({schema, Schema, aux_state}, maps:get(aux_state, V, undefined))
};
marshal(action, V) when is_list(V) ->
lists:foldl(fun apply_action/2, #mg_stateproc_ComplexAction{}, V);
marshal(action, V) ->
marshal(action, [V]);
marshal(timer, {timeout, V}) when V > 0 ->
{timeout, marshal(integer, V)};
marshal(deadline, {deadline, V}) ->
{deadline, marshal(timestamp, V)};
marshal(namespace, V) ->
marshal(atom, V);
marshal(id, V) ->
marshal(string, V);
marshal(event_id, V) ->
marshal(integer, V);
marshal(limit, V) ->
marshal({maybe, integer}, V);
marshal(direction, V) ->
marshal({enum, [forward, backward]}, V);
marshal({schema, Schema, T}, V) ->
% TODO
% Marshal properly
machinery_mg_schema:marshal(Schema, T, V);
marshal(timestamp, {{Date, Time}, USec} = V) ->
case rfc3339:format({Date, Time, USec, 0}) of
{ok, R} when is_binary(R) ->
R;
Error ->
error(badarg, {timestamp, V, Error})
end;
marshal({list, T}, V) when is_list(V) ->
[marshal(T, E) || E <- V];
marshal({maybe, _}, undefined) ->
undefined;
marshal({maybe, T}, V) ->
marshal(T, V);
marshal({enum, Choices = [_ | _]} = T, V) when is_atom(V) ->
_ = lists:member(V, Choices) orelse error(badarg, {T, V}),
V;
marshal(atom, V) when is_atom(V) ->
atom_to_binary(V, utf8);
marshal(string, V) when is_binary(V) ->
V;
marshal(integer, V) when is_integer(V) ->
V;
marshal(T, V) ->
error(badarg, {T, V}).
apply_action({set_timer, V}, CA) ->
CA#mg_stateproc_ComplexAction{
timer = {set_timer, #mg_stateproc_SetTimerAction{timer = marshal(timer, V)}}
};
apply_action(unset_timer, CA) ->
CA#mg_stateproc_ComplexAction{
timer = {unset_timer, #mg_stateproc_UnsetTimerAction{}}
};
apply_action(continue, CA) ->
CA#mg_stateproc_ComplexAction{
timer = {set_timer, #mg_stateproc_SetTimerAction{timer = {timeout, 0}}}
};
apply_action(remove, CA) ->
CA#mg_stateproc_ComplexAction{
remove = #mg_stateproc_RemoveAction{}
}.
%%
%% No unmarshalling for the decriptor required by the protocol so far.
%%
%% unmarshal(
%% descriptor,
%% #mg_stateproc_MachineDescriptor{
%% ns = NS,
%% ref = {'id', ID},
%% range = Range
%% }
%% ) ->
%% {unmarshal(namespace, NS), unmarshal(id, ID), unmarshal(range, Range)};
unmarshal(
range,
#mg_stateproc_HistoryRange{
'after' = Cursor,
'limit' = Limit,
'direction' = Direction
}
) ->
{unmarshal({maybe, event_id}, Cursor), unmarshal(limit, Limit), unmarshal(direction, Direction)};
unmarshal(
{machine, Schema},
#mg_stateproc_Machine{
'ns' = NS,
'id' = ID,
'history' = History,
'history_range' = Range,
'aux_state' = AuxState
}
) ->
#{
ns => unmarshal(namespace, NS),
id => unmarshal(id, ID),
history => unmarshal({history, Schema}, History),
range => unmarshal(range, Range),
aux_state => unmarshal({maybe, {schema, Schema, aux_state}}, AuxState)
};
unmarshal({history, Schema}, V) ->
unmarshal({list, {event, Schema}}, V);
unmarshal(
{event, Schema},
#mg_stateproc_Event{
'id' = EventID,
'created_at' = CreatedAt,
'event_payload' = Payload
}
) ->
{unmarshal(event_id, EventID), unmarshal(timestamp, CreatedAt), unmarshal({schema, Schema, event}, Payload)};
unmarshal({signal, Schema}, {init, #mg_stateproc_InitSignal{arg = Args}}) ->
{init, unmarshal({schema, Schema, {args, init}}, Args)};
unmarshal({signal, _Schema}, {timeout, #mg_stateproc_TimeoutSignal{}}) ->
timeout;
unmarshal({signal, Schema}, {repair, #mg_stateproc_RepairSignal{arg = Args}}) ->
{repair, unmarshal({maybe, {schema, Schema, {args, repair}}}, Args)};
unmarshal(namespace, V) ->
unmarshal(atom, V);
unmarshal(id, V) ->
unmarshal(string, V);
unmarshal(event_id, V) ->
unmarshal(integer, V);
unmarshal(limit, V) ->
unmarshal({maybe, integer}, V);
unmarshal(direction, V) ->
unmarshal({enum, [forward, backward]}, V);
unmarshal({schema, Schema, T}, V) ->
machinery_mg_schema:unmarshal(Schema, T, V);
unmarshal(timestamp, V) when is_binary(V) ->
case rfc3339:parse(V) of
{ok, {Date, Time, USec, TZOffset}} when TZOffset == undefined orelse TZOffset == 0 ->
{{Date, Time}, USec};
{ok, _} ->
error(badarg, {timestamp, V, badoffset});
{error, Reason} ->
error(badarg, {timestamp, V, Reason})
end;
unmarshal({list, T}, V) when is_list(V) ->
[unmarshal(T, E) || E <- V];
unmarshal({maybe, _}, undefined) ->
undefined;
unmarshal({maybe, T}, V) ->
unmarshal(T, V);
unmarshal({enum, Choices = [_ | _]} = T, V) when is_atom(V) ->
case lists:member(V, Choices) of
true ->
V;
false ->
error(badarg, {T, V})
end;
unmarshal(atom, V) when is_binary(V) ->
binary_to_existing_atom(V, utf8);
unmarshal(string, V) when is_binary(V) ->
V;
unmarshal(integer, V) when is_integer(V) ->
V;
unmarshal(T, V) ->
error(badarg, {T, V}).

View File

@ -0,0 +1,69 @@
%%%
%%% Simplistic machinegun client.
-module(machinery_mg_client).
-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl").
%% API
-export([new/2]).
-export([start/4]).
-export([call/3]).
-export([get_machine/2]).
-type woody_client() :: #{
url := woody:url(),
event_handler := woody:ev_handler(),
transport_opts => woody_client_thrift_http_transport:options()
}.
-opaque client() :: {woody_client(), woody_context:ctx()}.
-export_type([woody_client/0]).
-export_type([client/0]).
%%
-spec new(woody_client(), woody_context:ctx()) ->
client().
new(WoodyClient = #{url := _, event_handler := _}, WoodyCtx) ->
{WoodyClient, WoodyCtx}.
%%
-type namespace() :: mg_proto_base_thrift:'Namespace'().
-type id() :: mg_proto_base_thrift:'ID'().
-type args() :: mg_proto_state_processing_thrift:'Args'().
-type descriptor() :: mg_proto_state_processing_thrift:'MachineDescriptor'().
-type call_response() :: mg_proto_state_processing_thrift:'CallResponse'().
-type machine() :: mg_proto_state_processing_thrift:'Machine'().
-type namespace_not_found() :: mg_proto_state_processing_thrift:'NamespaceNotFound'().
-type machine_not_found() :: mg_proto_state_processing_thrift:'MachineNotFound'().
-type machine_already_exists() :: mg_proto_state_processing_thrift:'MachineAlreadyExists'().
-type machine_failed() :: mg_proto_state_processing_thrift:'MachineFailed'().
-spec start(namespace(), id(), args(), client()) ->
{ok, ok} |
{exception, namespace_not_found() | machine_already_exists() | machine_failed()}.
start(NS, ID, Args, Client) ->
issue_call('Start', [NS, ID, Args], Client).
-spec call(descriptor(), args(), client()) ->
{ok, call_response()} |
{exception, namespace_not_found() | machine_not_found() | machine_failed()}.
call(Descriptor, Args, Client) ->
issue_call('Call', [Descriptor, Args], Client).
-spec get_machine(descriptor(), client()) ->
{ok, machine()} |
{exception, namespace_not_found() | machine_not_found()}.
get_machine(Descriptor, Client) ->
issue_call('GetMachine', [Descriptor], Client).
%% Internal functions
issue_call(Function, Args, {WoodyClient, WoodyCtx}) ->
Service = {mg_proto_state_processing_thrift, 'Automaton'},
Request = {Service, Function, Args},
woody_client:call(Request, WoodyClient, WoodyCtx).

View File

@ -0,0 +1,46 @@
%%%
%%% Storage schema behaviour.
-module(machinery_mg_schema).
%% Behaviour definition
-type schema() :: module().
-type t() ::
{args,
init |
repair |
call
} |
response |
event |
aux_state.
-type v(T) ::
T.
-callback marshal(t(), v(_)) ->
machinery_msgpack:t().
-callback unmarshal(t(), machinery_msgpack:t()) ->
v(_).
-export_type([schema/0]).
-export_type([t/0]).
-export_type([v/1]).
%% API
-export([marshal/3]).
-export([unmarshal/3]).
-spec marshal(schema(), t(), v(_)) ->
machinery_msgpack:t().
marshal(Schema, T, V) ->
Schema:marshal(T, V).
-spec unmarshal(schema(), t(), machinery_msgpack:t()) ->
v(_).
unmarshal(Schema, T, V) ->
Schema:unmarshal(T, V).

View File

@ -0,0 +1,97 @@
%%%
%%% Storage schema adapted to store arbitrary persistent Erlang terms.
%%%
%%% Excluding, as expected: pids, refs, ports.
-module(machinery_mg_schema_generic).
%% API
-export([marshal/1]).
-export([unmarshal/1]).
%% Storage schema behaviour
-behaviour(machinery_mg_schema).
-export([marshal/2]).
-export([unmarshal/2]).
-import(machinery_msgpack, [
nil/0,
wrap/1,
unwrap/1
]).
%%
-type t() :: machinery_mg_schema:t().
-type v(T) :: machinery_mg_schema:v(T).
-spec marshal(t(), v(eterm())) ->
machinery_msgpack:t().
marshal(_T, V) ->
marshal(V).
-spec unmarshal(t(), machinery_msgpack:t()) ->
v(eterm()).
unmarshal(_T, V) ->
unmarshal(V).
%% API
-type eterm() ::
atom() |
number() |
tuple() |
binary() |
list() |
map().
-spec marshal(eterm()) ->
machinery_msgpack:t().
marshal(undefined) ->
nil();
marshal(V) when is_boolean(V) ->
wrap(V);
marshal(V) when is_atom(V) ->
wrap(atom_to_binary(V, utf8));
marshal(V) when is_number(V) ->
wrap(V);
marshal(V) when is_binary(V) ->
wrap({binary, V});
marshal([]) ->
wrap([]);
marshal(V) when is_list(V) ->
wrap([marshal(lst) | lists:map(fun marshal/1, V)]);
marshal(V) when is_tuple(V) ->
wrap([marshal(tup) | lists:map(fun marshal/1, tuple_to_list(V))]);
marshal(V) when is_map(V) ->
wrap([marshal(map), wrap(genlib_map:truemap(fun (Ke, Ve) -> {marshal(Ke), marshal(Ve)} end, V))]);
marshal(V) ->
error({badarg, V}).
-spec unmarshal(machinery_msgpack:t()) ->
eterm().
unmarshal(M) ->
unmarshal_v(unwrap(M)).
unmarshal_v(nil) ->
undefined;
unmarshal_v(V) when is_boolean(V) ->
V;
unmarshal_v(V) when is_binary(V) ->
binary_to_existing_atom(V, utf8);
unmarshal_v(V) when is_number(V) ->
V;
unmarshal_v({binary, V}) ->
V;
unmarshal_v([]) ->
[];
unmarshal_v([Ty | Vs]) ->
unmarshal_v(unmarshal(Ty), Vs).
unmarshal_v(lst, Vs) ->
lists:map(fun unmarshal/1, Vs);
unmarshal_v(tup, Es) ->
list_to_tuple(unmarshal_v(lst, Es));
unmarshal_v(map, [V]) ->
genlib_map:truemap(fun (Ke, Ve) -> {unmarshal(Ke), unmarshal(Ve)} end, unwrap(V)).

81
src/machinery_msgpack.erl Normal file
View File

@ -0,0 +1,81 @@
%%%
%%% Msgpack manipulation employed by machinegun interfaces.
-module(machinery_msgpack).
-include_lib("mg_proto/include/mg_proto_msgpack_thrift.hrl").
%% API
-export([wrap/1]).
-export([unwrap/1]).
-export([nil/0]).
-type t() :: mg_proto_msgpack_thrift:'Value'().
-export_type([t/0]).
%%
-spec wrap
(nil ) -> t();
(boolean() ) -> t();
(integer() ) -> t();
(float() ) -> t();
(binary() ) -> t(); %% string
({binary, binary()}) -> t(); %% binary
([t()] ) -> t();
(#{t() => t()} ) -> t().
wrap(nil) ->
{nl, #mg_msgpack_Nil{}};
wrap(V) when is_boolean(V) ->
{b, V};
wrap(V) when is_integer(V) ->
{i, V};
wrap(V) when is_float(V) ->
V;
wrap(V) when is_binary(V) ->
% Assuming well-formed UTF-8 bytestring.
{str, V};
wrap({binary, V}) when is_binary(V) ->
{bin, V};
wrap(V) when is_list(V) ->
{arr, V};
wrap(V) when is_map(V) ->
{obj, V}.
-spec unwrap(t()) ->
nil |
boolean() |
integer() |
float() |
binary() | %% string
{binary, binary()} | %% binary
[t()] |
#{t() => t()} .
unwrap({nl, #mg_msgpack_Nil{}}) ->
nil;
unwrap({b, V}) when is_boolean(V) ->
V;
unwrap({i, V}) when is_integer(V) ->
V;
unwrap({flt, V}) when is_float(V) ->
V;
unwrap({str, V}) when is_binary(V) ->
% Assuming well-formed UTF-8 bytestring.
V;
unwrap({bin, V}) when is_binary(V) ->
{binary, V};
unwrap({arr, V}) when is_list(V) ->
V;
unwrap({obj, V}) when is_map(V) ->
V.
%%
-spec nil() -> t().
nil() ->
wrap(nil).

75
src/machinery_utils.erl Normal file
View File

@ -0,0 +1,75 @@
-module(machinery_utils).
%% Types
-type woody_routes() :: [woody_server_thrift_http_handler:route(_)].
-type woody_handler() :: woody:http_handler(woody:th_handler()).
-type handler(T) :: T.
-type get_woody_handler() :: fun((handler(_), route_opts()) -> woody_handler()).
-type woody_server_config() :: #{
ip := inet:ip_address(),
port := inet:port_number(),
net_opts => cowboy_protocol:opts()
}.
-type route_opts() :: #{
event_handler := woody:ev_handler(),
handler_limits => woody_server_thrift_http_handler:handler_limits()
}.
-export_type([woody_server_config/0]).
-export_type([woody_routes/0]).
-export_type([woody_handler/0]).
-export_type([route_opts/0]).
-export_type([handler/1]).
-export_type([get_woody_handler/0]).
%% API
-export([get_handler/1]).
-export([get_backend/1]).
-export([expand_modopts/2]).
-export([woody_child_spec/3]).
-export([get_woody_routes/3]).
%% API
-spec get_handler(machinery:modopts(Opts)) ->
{module(), Opts}.
get_handler(Handler) ->
expand_modopts(Handler, undefined).
-spec get_backend(machinery:backend(Opts)) ->
{module(), Opts}.
get_backend(Backend) ->
expand_modopts(Backend, #{}).
-spec expand_modopts(machinery:modopts(Opts), Opts) ->
{module(), Opts}.
expand_modopts({Mod, Opts}, _) ->
{Mod, Opts};
expand_modopts(Mod, Opts) ->
{Mod, Opts}.
-spec woody_child_spec(_Id, woody_routes(), woody_server_config()) ->
supervisor:child_spec().
woody_child_spec(Id, Routes, Config) ->
woody_server:child_spec(Id, Config#{
%% ev handler for `handlers`, which is `[]`, so this is just to satisfy the spec.
event_handler => woody_event_handler_default,
handlers => [],
additional_routes => Routes
}).
-spec get_woody_routes([handler(_)], get_woody_handler(), route_opts()) ->
woody_routes().
get_woody_routes(Handlers, GetHandler, Opts = #{event_handler := _}) ->
woody_server_thrift_http_handler:get_routes(Opts#{
handlers => [GetHandler(H, Opts) || H <- Handlers]
}).

160
test/ct_helper.erl Normal file
View File

@ -0,0 +1,160 @@
-module(ct_helper).
-export([cfg/2]).
-export([start_apps/1]).
-export([start_app/1]).
-export([stop_apps/1]).
-export([stop_app/1]).
-export([makeup_cfg/2]).
-export([woody_ctx/0]).
-export([get_woody_ctx/1]).
-export([test_case_name/1]).
-export([get_test_case_name/1]).
-type test_case_name() :: atom().
-type group_name() :: atom().
-type config() :: [{atom(), term()}].
-export_type([test_case_name/0]).
-export_type([group_name/0]).
-export_type([config/0]).
%%
-spec cfg(atom(), config()) -> term().
cfg(Key, Config) ->
case lists:keyfind(Key, 1, Config) of
{Key, V} -> V;
_ -> error({undefined, Key, Config})
end.
%%
-type app_name() :: atom().
-type app_env() :: [{atom(), term()}].
-type startup_ctx() :: #{atom() => _}.
-spec start_apps([app_name()]) -> {[Started :: app_name()], startup_ctx()}.
start_apps(AppNames) ->
lists:foldl(
fun (AppName, {SAcc, CtxAcc}) ->
{Started, Ctx} = start_app(AppName),
{SAcc ++ Started, maps:merge(CtxAcc, Ctx)}
end,
{[], #{}},
AppNames
).
-spec start_app(app_name()) -> {[Started :: app_name()], startup_ctx()}.
start_app(lager = AppName) ->
{start_app_with(AppName, [
{async_threshold, 1},
{async_threshold_window, 0},
{error_logger_hwm, 600},
{suppress_application_start_stop, true},
{suppress_supervisor_start_stop, true},
{handlers, [
{lager_common_test_backend, debug}
]}
]), #{}};
start_app(scoper = AppName) ->
{start_app_with(AppName, [
{storage, scoper_storage_lager}
]), #{}};
start_app(woody = AppName) ->
{start_app_with(AppName, [
{acceptors_pool_size, 4}
]), #{}};
start_app(AppName) ->
{start_app_with(AppName, []), #{}}.
-spec start_app_with(app_name(), app_env()) -> {[app_name()], #{atom() => _}}.
start_app_with(AppName, Env) ->
_ = application:load(AppName),
_ = set_app_env(AppName, Env),
case application:ensure_all_started(AppName) of
{ok, Apps} ->
Apps;
{error, Reason} ->
exit({start_app_failed, AppName, Reason})
end.
set_app_env(AppName, Env) ->
lists:foreach(
fun ({K, V}) ->
ok = application:set_env(AppName, K, V)
end,
Env
).
-spec stop_apps([app_name()]) -> ok.
stop_apps(AppNames) ->
lists:foreach(fun stop_app/1, lists:reverse(AppNames)).
-spec stop_app(app_name()) -> ok.
stop_app(AppName) ->
case application:stop(AppName) of
ok ->
case application:unload(AppName) of
ok ->
ok;
{error, Reason} ->
exit({unload_app_failed, AppName, Reason})
end;
{error, Reason} ->
exit({unload_app_failed, AppName, Reason})
end.
%%
-type config_mut_fun() :: fun((config()) -> config()).
-spec makeup_cfg([config_mut_fun()], config()) -> config().
makeup_cfg(CMFs, C0) ->
lists:foldl(fun (CMF, C) -> CMF(C) end, C0, CMFs).
-spec woody_ctx() -> config_mut_fun().
woody_ctx() ->
fun (C) -> [{'$woody_ctx', construct_woody_ctx(C)} | C] end.
construct_woody_ctx(C) ->
woody_context:new(construct_rpc_id(get_test_case_name(C))).
construct_rpc_id(TestCaseName) ->
woody_context:new_rpc_id(
<<"undefined">>,
list_to_binary(lists:sublist(atom_to_list(TestCaseName), 32)),
woody_context:new_req_id()
).
-spec get_woody_ctx(config()) -> woody_context:ctx().
get_woody_ctx(C) ->
cfg('$woody_ctx', C).
%%
-spec test_case_name(test_case_name()) -> config_mut_fun().
test_case_name(TestCaseName) ->
fun (C) -> [{'$test_case_name', TestCaseName} | C] end.
-spec get_test_case_name(config()) -> test_case_name().
get_test_case_name(C) ->
cfg('$test_case_name', C).

32
test/ct_sup.erl Normal file
View File

@ -0,0 +1,32 @@
-module(ct_sup).
-export([start/0]).
-export([stop/1]).
%%
-behaviour(supervisor).
-export([init/1]).
%%
-spec start() -> pid().
start() ->
{ok, PID} = supervisor:start_link(?MODULE, []),
true = unlink(PID),
PID.
-spec stop(pid()) -> ok.
stop(PID) ->
true = exit(PID, shutdown),
ok.
%%
-spec init([]) ->
{ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init([]) ->
{ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}.

View File

@ -0,0 +1,6 @@
namespaces:
payproc/tags:
processor:
url: http://machinery:8022/v1/stateproc
storage:
type: memory

View File

@ -0,0 +1,151 @@
%%% TODO
%%% - Model things like `ct_sup` with something hook-like?
-module(machinery_machine_unique_tag_mg_example_test_SUITE).
%% Test suite
-export([all/0]).
-export([init_per_suite/1]).
-export([end_per_suite/1]).
-export([init_per_testcase/2]).
-export([tag_success/1]).
-export([tag_twice_success/1]).
-export([single_tag_set_only/1]).
-export([untag_success/1]).
-export([conflict_untag_failure/1]).
-export([reset_tag_success/1]).
-import(ct_helper, [
cfg/2,
start_apps/1,
get_woody_ctx/1
]).
%%
-type config() :: ct_helper:config().
-type test_case_name() :: ct_helper:test_case_name().
-type group_name() :: ct_helper:group_name().
-type test_return() :: _ | no_return().
-spec all() -> [test_case_name() | {group, group_name()}].
all() ->
[
tag_success ,
tag_twice_success ,
single_tag_set_only ,
untag_success ,
conflict_untag_failure ,
reset_tag_success
].
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
% _ = dbg:tracer(),
% _ = dbg:p(all, c),
% _ = dbg:tpl({'woody_client', '_', '_'}, x),
{StartedApps, _StartupCtx} = start_apps([lager, machinery]),
SuiteSup = ct_sup:start(),
start_woody_server([
{started_apps , StartedApps},
{suite_sup , SuiteSup}
| C]).
start_woody_server(C) ->
{ok, PID} = supervisor:start_child(
cfg(suite_sup, C),
machinery_machine_unique_tag_mg_example:child_spec(?MODULE)
),
[{payproc_mg_machine_sup, PID} | C].
-spec end_per_suite(config()) -> _.
end_per_suite(C) ->
ok = ct_sup:stop(cfg(suite_sup, C)),
ok = ct_helper:stop_apps(cfg(started_apps, C)),
ok.
-spec init_per_testcase(test_case_name(), config()) -> config().
init_per_testcase(TestCaseName, C) ->
ct_helper:makeup_cfg([ct_helper:test_case_name(TestCaseName), ct_helper:woody_ctx()], C).
%%
-spec tag_success(config()) -> test_return().
tag_success(C) ->
Tag = genlib:unique(),
ID = pid_to_binary(self()),
Opts = #{woody_ctx => get_woody_ctx(C)},
ok = machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID, Opts),
{ok, ID} = machinery_machine_unique_tag_mg_example:get(payproc, Tag, Opts).
-spec tag_twice_success(config()) -> test_return().
tag_twice_success(C) ->
Tag = genlib:unique(),
ID = pid_to_binary(self()),
Opts = #{woody_ctx => get_woody_ctx(C)},
ok = machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID, Opts),
ok = machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID, Opts),
{ok, ID} = machinery_machine_unique_tag_mg_example:get(payproc, Tag, Opts).
-spec single_tag_set_only(config()) -> test_return().
single_tag_set_only(C) ->
Tag = genlib:unique(),
Opts = #{woody_ctx => get_woody_ctx(C)},
IDs = [integer_to_binary(E) || E <- lists:seq(1, 42)],
Rs = genlib_pmap:map(
fun (ID) ->
{ID, machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID, Opts)}
end,
IDs
),
[IDSet] = [ID0 || {ID0, ok} <- Rs],
IDsLeft = IDs -- [IDSet],
IDsLeft = [ID0 || {ID0, {error, {set, ID}}} <- Rs, ID == IDSet].
-spec untag_success(config()) -> test_return().
untag_success(C) ->
Tag = genlib:unique(),
ID = pid_to_binary(self()),
Opts = #{woody_ctx => get_woody_ctx(C)},
ok = machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID, Opts),
ok = machinery_machine_unique_tag_mg_example:untag(payproc, Tag, ID, Opts),
{error, unset} = machinery_machine_unique_tag_mg_example:get(payproc, Tag, Opts).
-spec conflict_untag_failure(config()) -> test_return().
conflict_untag_failure(C) ->
Tag = genlib:unique(),
ID1 = pid_to_binary(self()),
ID2 = pid_to_binary(cfg(suite_sup, C)),
Opts = #{woody_ctx => get_woody_ctx(C)},
ok = machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID1, Opts),
{error, {set, ID1}} = machinery_machine_unique_tag_mg_example:untag(payproc, Tag, ID2, Opts),
ok = machinery_machine_unique_tag_mg_example:untag(payproc, Tag, ID1, Opts),
ok = machinery_machine_unique_tag_mg_example:untag(payproc, Tag, ID2, Opts).
-spec reset_tag_success(config()) -> test_return().
reset_tag_success(C) ->
Tag = genlib:unique(),
ID = pid_to_binary(self()),
Opts = #{woody_ctx => get_woody_ctx(C)},
ok = machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID, Opts),
ok = machinery_machine_unique_tag_mg_example:untag(payproc, Tag, ID, Opts),
ok = machinery_machine_unique_tag_mg_example:untag(payproc, Tag, ID, Opts),
ok = machinery_machine_unique_tag_mg_example:tag(payproc, Tag, ID, Opts),
{ok, ID} = machinery_machine_unique_tag_mg_example:get(payproc, Tag, Opts).
%%
pid_to_binary(PID) ->
list_to_binary(pid_to_list(PID)).