mirror of
https://github.com/valitydev/machinery-erlang.git
synced 2024-11-06 00:35:19 +00:00
Introduce machinegun client/processor handler lib (#1)
This commit is contained in:
parent
0d9dc7c155
commit
7dac1c80f3
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal 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
53
Jenkinsfile
vendored
Normal 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
48
Makefile
Normal 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
|
@ -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
28
docker-compose.sh
Executable 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
77
elvis.config
Normal 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
71
rebar.config
Normal 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
44
rebar.lock
Normal 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
23
src/machinery.app.src
Normal 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
150
src/machinery.erl
Normal 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
45
src/machinery_backend.erl
Normal 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).
|
120
src/machinery_machine_unique_tag.erl
Normal file
120
src/machinery_machine_unique_tag.erl
Normal 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.
|
96
src/machinery_machine_unique_tag_mg_example.erl
Normal file
96
src/machinery_machine_unique_tag_mg_example.erl
Normal 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
|
||||
}
|
||||
}.
|
497
src/machinery_mg_backend.erl
Normal file
497
src/machinery_mg_backend.erl
Normal 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}).
|
69
src/machinery_mg_client.erl
Normal file
69
src/machinery_mg_client.erl
Normal 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).
|
46
src/machinery_mg_schema.erl
Normal file
46
src/machinery_mg_schema.erl
Normal 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).
|
97
src/machinery_mg_schema_generic.erl
Normal file
97
src/machinery_mg_schema_generic.erl
Normal 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
81
src/machinery_msgpack.erl
Normal 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
75
src/machinery_utils.erl
Normal 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
160
test/ct_helper.erl
Normal 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
32
test/ct_sup.erl
Normal 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}, []}}.
|
6
test/machinegun/config.yaml
Normal file
6
test/machinegun/config.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
namespaces:
|
||||
payproc/tags:
|
||||
processor:
|
||||
url: http://machinery:8022/v1/stateproc
|
||||
storage:
|
||||
type: memory
|
151
test/machinery_machine_unique_tag_mg_example_test_SUITE.erl
Normal file
151
test/machinery_machine_unique_tag_mg_example_test_SUITE.erl
Normal 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)).
|
Loading…
Reference in New Issue
Block a user