将 Learn You Some Erlang 教程从 gen_fsm 转换为 gen_statem

Posted

技术标签:

【中文标题】将 Learn You Some Erlang 教程从 gen_fsm 转换为 gen_statem【英文标题】:Converting Learn You Some Erlang tutorial from gen_fsm to gen_statem 【发布时间】:2021-08-12 23:07:32 【问题描述】:

我一直在阅读本教程的 Rage Against The Finite State Machine 章节,它使用了 gen_fsm,它已被弃用,取而代之的是 gen_statem。运行测试时,我总是卡住,因为其中一个客户端处于 negotiate 状态 并收到 accept_negotiate 事件。 (也许还有其他错误,但现在我无法弄清楚为什么会这样)。

trade_statem.erl

-module(trade_statem).
-behaviour(gen_statem).

% Public API
-export([start/1, start_link/1, trade/2, accept_trade/1, make_offer/2, retract_offer/2, ready/1, cancel/1]).
% gen_statem callbacks
-export([init/1, callback_mode/0, code_change/4, terminate/3]).
% Custom states
-export([idle/3, idle_wait/3, negotiate/3, wait/3, ready/3]).

% Data record
-record(data, name = "", other, own_items = [], other_items = [], monitor, from).

% Public API
start(Name) -> gen_statem:start(?MODULE, [Name], []).

start_link(Name) -> gen_statem:start_link(?MODULE, [Name], []).

trade(OwnPid, OtherPid) -> gen_statem:call(OwnPid, negotiate, OtherPid, 30000).

accept_trade(OwnPid) -> gen_statem:call(OwnPid, accept_negotiate).

make_offer(OwnPid, Item) -> gen_statem:cast(OwnPid, make_offer, Item).

retract_offer(OwnPid, Item) -> gen_statem:cast(OwnPid, retract_offer, Item).

ready(OwnPid) -> gen_statem:call(OwnPid, ready, infinity).

cancel(OwnPid) -> gen_statem:stop(OwnPid).

% Client-To-Client API
ask_negotiate(OtherPid, OwnPid) -> gen_statem:cast(OtherPid, ask_negotiate, OwnPid).

accept_negotiate(OtherPid, OwnPid) -> gen_statem:cast(OtherPid, accept_negotiate, OwnPid).

do_offer(OtherPid, Item) -> gen_statem:cast(OtherPid, do_offer, Item).

undo_offer(OtherPid, Item) -> gen_statem:cast(OtherPid, undo_offer, Item).

are_you_ready(OtherPid) -> gen_statem:cast(OtherPid, are_you_ready).

not_yet(OtherPid) -> gen_statem:cast(OtherPid, not_yet).

am_ready(OtherPid) -> gen_statem:cast(OtherPid, ready).

ack_trans(OtherPid) -> gen_statem:cast(OtherPid, ack).

ask_commit(OtherPid) -> gen_statem:call(OtherPid, ask_commit).

do_commit(OtherPid) -> gen_statem:call(OtherPid, do_commit).

% gen_statem API
init(Name) -> ok, idle, #dataname = Name.

callback_mode() -> state_functions.

code_change(_, StateName, Data, _) -> ok, StateName, Data.

terminate(normal, ready, D = #data) -> notice(D, "FSM leaving.", []);
terminate(_, _, _) -> ok.

% Custom states
idle(cast, ask_negotiate, OtherPid, D = #data) ->
    Ref = monitor(process, OtherPid),
    notice(D, "~p asked for a trade negotiation", [OtherPid]),
    next_state, idle_wait, D#dataother = OtherPid, monitor = Ref;
idle(call, From, negotiate, OtherPid, D = #data) ->
    ask_negotiate(OtherPid, self()),
    notice(D, "asking user ~p for a trade", [OtherPid]),
    Ref = monitor(process, OtherPid),
    next_state, idle_wait, D#dataother = OtherPid, monitor = Ref, from = From;
idle(_, Event, _) ->
    unexpected(Event, idle),
    keep_state_and_data.

idle_wait(cast, ask_negotiate, OtherPid, D = #dataother = OtherPid) ->
    gen_statem:reply(D#data.from, ok),
    notice(D, "starting negotiation", []),
    next_state, negotiate, D;
idle_wait(cast, accept_negotiate, OtherPid, D = #dataother = OtherPid) ->
    accept_negotiate(OtherPid, self()),
    notice(D, "accepting negotiation", []),
    next_state, negotiate, D;
idle_wait(call, From, accept_negotiate, D = #dataother = OtherPid) ->
    accept_negotiate(OtherPid, self()),
    notice(D, "accepting negotiation", []),
    next_state, negotiate, D, reply, From, ok;
idle_wait(_, Event, _) ->
    unexpected(Event, idle_wait),
    keep_state_and_data.

negotiate(cast, make_offer, Item, D = #dataown_items = OwnItems) ->
    do_offer(D#data.other, Item),
    notice(D, "offering ~p", [Item]),
    keep_state, D#dataown_items = add(Item, OwnItems);
negotiate(cast, retract_offer, Item, D = #dataown_items = OwnItems) ->
    undo_offer(D#data.other, Item),
    notice(D, "cancelling offer on ~p", [Item]),
    keep_state, D#dataown_items = remove(Item, OwnItems);
negotiate(cast, do_offer, Item, D = #dataother_items = OtherItems) ->
    notice(D, "other player offering ~p", [Item]),
    keep_state, D#dataother_items = add(Item, OtherItems);
negotiate(cast, undo_offer, Item, D = #dataother_items = OtherItems) ->
    notice(D, "Other player cancelling offer on ~p", [Item]),
    keep_state, D#dataother_items = remove(Item, OtherItems);
negotiate(cast, are_you_ready, D = #dataother = OtherPid) ->
    io:format("Other user ready to trade~n"),
    notice(D, "Other user ready to transfer goods:~nYou get ~p, the other side gets ~p", [D#data.other_items, D#data.own_items]),
    not_yet(OtherPid),
    keep_state_and_data;
negotiate(call, From, ready, D = #dataother = OtherPid) ->
    are_you_ready(OtherPid),
    notice(D, "asking if ready, waiting", []),
    next_state, wait, D#datafrom = From;
negotiate(EventType, Event, Data) ->
    unexpected(EventType, Event, Data, negotiate),
    keep_state_and_data.

wait(cast, are_you_ready, D = #data) ->
    am_ready(D#data.other),
    notice(D, "asked if ready, and I am. Waiting for same reply", []),
    keep_state_and_data;
wait(cast, not_yet, D = #data) ->
    notice(D, "Other not ready yet", []),
    keep_state_and_data;
wait(cast, ready, D = #data) ->
    am_ready(D#data.other),
    ack_trans(D#data.other),
    gen_statem:reply(D#data.from, ok),
    notice(D, "other side is ready. Moving to ready state", []),
    next_state, ready, D;
wait(_, Event, _) ->
    unexpected(Event, wait),
    keep_state_and_data.

ready(call, _, ack, D = #data) ->
    case priority(self(), D#data.other) of
        true ->
            try
                notice(D, "asking for commit", []),
                ready_commit = ask_commit(D#data.other),
                notice(D, "ordering commit", []),
                ok = do_commit(D#data.other),
                notice(D, "committing...", []),
                commit(D),
                % Sus
                stop, normal, D
            catch Class:Reason ->
                notice(D, "commit failed", []),
                % Sus
                stop, Class, Reason, D
            end;
        false ->
            keep_state_and_data
    end;
ready(call, From, ask_commit, D) ->
    notice(D, "replying to ask commit", []),
    % Sus
    keep_state_and_data, reply, From, ready_commit;
ready(call, _, do_commit, D) ->
    notice(D, "committing...", []),
    commit(D),
    % Sus
    stop, normal, ok, D;
ready(_, Event, _) ->
    unexpected(Event, ready),
    keep_state_and_data.

% Private functions
add (Item, Items) -> [Item | Items].

remove(Item, Items) -> Items -- [Item].

notice(#dataname = N, Str, Args) -> io:format("~s: " ++ Str ++ "~n", [N | Args]).

unexpected(Msg, State) -> io:format("~p received unknown event ~p while in state ~p~n", [self(), Msg, State]).

priority(OwnPid, OtherPid) when OwnPid > OtherPid -> true;
priority(OwnPid, OtherPid) when OwnPid < OtherPid -> false.

commit(D = #data) -> io:format("Transaction completed for ~s. Items sent are:~n~p,~n received are:~n~p.~nThis operation should have some atomic save in a database.~n", [D#data.name, D#data.own_items, D#data.other_items]).

trade_calls.erl

-module(trade_calls).
-export([main_ab/0, main_cd/0, main_ef/0]).

%% test a little bit of everything and also deadlocks on ready state
%% -- leftover messages possible on race conditions on ready state
main_ab() ->
    S = self(),
    PidCliA = spawn(fun() -> a(S) end),
    receive PidA -> PidA end,
    spawn(fun() -> b(PidA, PidCliA) end).

a(Parent) ->
    ok, Pid = trade_statem:start_link("Carl"),
    Parent ! Pid,
    io:format("Spawned Carl: ~p~n", [Pid]),
    %sys:trace(Pid,true),
    timer:sleep(800),
    trade_statem:accept_trade(Pid),
    timer:sleep(400),
    io:format("~p~n",[trade_statem:ready(Pid)]),
    timer:sleep(1000),
    trade_statem:make_offer(Pid, "horse"),
    trade_statem:make_offer(Pid, "sword"),
    timer:sleep(1000),
    io:format("a synchronizing~n"),
    sync2(),
    trade_statem:ready(Pid),
    timer:sleep(200),
    trade_statem:ready(Pid),
    timer:sleep(1000).

b(PidA, PidCliA) ->
    ok, Pid = trade_statem:start_link("Jim"),
    io:format("Spawned Jim: ~p~n", [Pid]),
    %sys:trace(Pid,true),
    timer:sleep(500),
    trade_statem:trade(Pid, PidA),
    trade_statem:make_offer(Pid, "boots"),
    timer:sleep(200),
    trade_statem:retract_offer(Pid, "boots"),
    timer:sleep(500),
    trade_statem:make_offer(Pid, "shotgun"),
    timer:sleep(1000),
    io:format("b synchronizing~n"),
    sync1(PidCliA),
    trade_statem:make_offer(Pid, "horse"), %% race condition!
    trade_statem:ready(Pid),
    timer:sleep(200),
    timer:sleep(1000).

%% force a race condition on cd trade negotiation
main_cd() ->
    S = self(),
    PidCliC = spawn(fun() -> c(S) end),
    receive PidC -> PidC end,
    spawn(fun() -> d(S, PidC, PidCliC) end),
    receive PidD -> PidD end,
    PidCliC ! PidD.
    
c(Parent) ->
    ok, Pid = trade_statem:start_link("Marc"),
    Parent ! Pid,
    receive PidD -> PidD end,
    io:format("Spawned Marc: ~p~n", [Pid]),
    %sys:trace(Pid, true),
    sync2(),
    trade_statem:trade(Pid, PidD),
    %% no need to accept_trade thanks to the race condition
    timer:sleep(200),
    trade_statem:retract_offer(Pid, "car"),
    trade_statem:make_offer(Pid, "horse"),
    timer:sleep(600),
    trade_statem:cancel(Pid),
    timer:sleep(1000).

d(Parent, PidC, PidCliC) ->
    ok, Pid = trade_statem:start_link("Pete"),
    Parent ! Pid,
    io:format("Spawned Jim: ~p~n", [Pid]),
    %sys:trace(Pid,true),
    sync1(PidCliC),
    trade_statem:trade(Pid, PidC),
    %% no need to accept_trade thanks to the race condition
    timer:sleep(200),
    trade_statem:retract_offer(Pid, "car"),
    trade_statem:make_offer(Pid, "manatee"),
    timer:sleep(100),
    trade_statem:ready(Pid),
    timer:sleep(1000).

main_ef() ->
    S = self(),
    PidCliE = spawn(fun() -> e(S) end),
    receive PidE -> PidE end,
    spawn(fun() -> f(PidE, PidCliE) end).

e(Parent) ->
    ok, Pid = trade_statem:start_link("Carl"),
    Parent ! Pid,
    io:format("Spawned Carl: ~p~n", [Pid]),
    %sys:trace(Pid,true),
    timer:sleep(800),
    trade_statem:accept_trade(Pid),
    timer:sleep(400),
    io:format("~p~n",[trade_statem:ready(Pid)]),
    timer:sleep(1000),
    trade_statem:make_offer(Pid, "horse"),
    trade_statem:make_offer(Pid, "sword"),
    timer:sleep(1000),
    io:format("a synchronizing~n"),
    sync2(),
    trade_statem:ready(Pid),
    timer:sleep(200),
    trade_statem:ready(Pid),
    timer:sleep(1000).

f(PidE, PidCliE) ->
    ok, Pid = trade_statem:start_link("Jim"),
    io:format("Spawned Jim: ~p~n", [Pid]),
    %sys:trace(Pid,true),
    timer:sleep(500),
    trade_statem:trade(Pid, PidE),
    trade_statem:make_offer(Pid, "boots"),
    timer:sleep(200),
    trade_statem:retract_offer(Pid, "boots"),
    timer:sleep(500),
    trade_statem:make_offer(Pid, "shotgun"),
    timer:sleep(1000),
    io:format("b synchronizing~n"),
    sync1(PidCliE),
    trade_statem:make_offer(Pid, "horse"),
    timer:sleep(200),
    trade_statem:ready(Pid),
    timer:sleep(1000).

%%% Utils
sync1(Pid) ->
    Pid ! self(),
    receive ack -> ok end.

sync2() ->
    receive
        From -> From ! ack
    end.

感谢您的宝贵时间!

【问题讨论】:

【参考方案1】:

有一些错误,您可以在它们上运行diff 以获取差异。您可能会在交易结束时看到一些 otp 报告,但它们完全是意料之中的。

trade_statem.erl

-module(trade_statem).
-behaviour(gen_statem).

% Public API
-export([start/1, start_link/1, trade/2, accept_trade/1, make_offer/2, retract_offer/2, ready/1, cancel/1]).
% gen_statem callbacks
-export([init/1, callback_mode/0, code_change/4, terminate/3]).
% Custom states
-export([idle/3, idle_wait/3, negotiate/3, wait/3, ready/3]).

% Data record
-record(data, name = "", other, own_items = [], other_items = [], monitor, from).

% Public API
start(Name) -> gen_statem:start(?MODULE, [Name], []).

start_link(Name) -> gen_statem:start_link(?MODULE, [Name], []).

trade(OwnPid, OtherPid) -> gen_statem:call(OwnPid, negotiate, OtherPid, 30000).

accept_trade(OwnPid) -> gen_statem:call(OwnPid, accept_negotiate).

make_offer(OwnPid, Item) -> gen_statem:cast(OwnPid, make_offer, Item).

retract_offer(OwnPid, Item) -> gen_statem:cast(OwnPid, retract_offer, Item).

ready(OwnPid) -> gen_statem:call(OwnPid, ready, infinity).

cancel(OwnPid) -> gen_statem:stop(OwnPid).

% Client-To-Client API
ask_negotiate(OtherPid, OwnPid) -> gen_statem:cast(OtherPid, ask_negotiate, OwnPid).

accept_negotiate(OtherPid, OwnPid) -> gen_statem:cast(OtherPid, accept_negotiate, OwnPid).

do_offer(OtherPid, Item) -> gen_statem:cast(OtherPid, do_offer, Item).

undo_offer(OtherPid, Item) -> gen_statem:cast(OtherPid, undo_offer, Item).

are_you_ready(OtherPid) -> gen_statem:cast(OtherPid, are_you_ready).

not_yet(OtherPid) -> gen_statem:cast(OtherPid, not_yet).

am_ready(OtherPid) -> gen_statem:cast(OtherPid, ready).

ack_trans(OtherPid) -> gen_statem:cast(OtherPid, ack).

ask_commit(OtherPid) -> gen_statem:call(OtherPid, ask_commit).

do_commit(OtherPid) -> gen_statem:call(OtherPid, do_commit).

% gen_statem API
init(Name) -> ok, idle, #dataname = Name.

callback_mode() -> state_functions.

code_change(_, StateName, Data, _) -> ok, StateName, Data.

terminate(normal, ready, D = #data) -> notice(D, "FSM leaving.", []);
terminate(_, _, _) -> ok.

% Custom states
idle(cast, ask_negotiate, OtherPid, D = #data) ->
    Ref = monitor(process, OtherPid),
    notice(D, "~p asked for a trade negotiation", [OtherPid]),
    next_state, idle_wait, D#dataother = OtherPid, monitor = Ref;
idle(call, From, negotiate, OtherPid, D = #data) ->
    ask_negotiate(OtherPid, self()),
    notice(D, "asking user ~p for a trade", [OtherPid]),
    Ref = monitor(process, OtherPid),
    next_state, idle_wait, D#dataother = OtherPid, monitor = Ref, from = From;
idle(_, Event, _) ->
    unexpected(Event, idle),
    keep_state_and_data.

idle_wait(cast, ask_negotiate, OtherPid, D = #dataother = OtherPid) ->
    gen_statem:reply(D#data.from, ok),
    notice(D, "starting negotiation", []),
    next_state, negotiate, D;
idle_wait(cast, accept_negotiate, OtherPid, D = #dataother = OtherPid) ->
    gen_statem:reply(D#data.from, ok),
    notice(D, "starting negotiation", []),
    next_state, negotiate, D;
idle_wait(call, From, accept_negotiate, D = #dataother = OtherPid) ->
    accept_negotiate(OtherPid, self()),
    notice(D, "accepting negotiation", []),
    next_state, negotiate, D, reply, From, ok;
idle_wait(_, Event, _) ->
    unexpected(Event, idle_wait),
    keep_state_and_data.

negotiate(cast, make_offer, Item, D = #dataown_items = OwnItems) ->
    do_offer(D#data.other, Item),
    notice(D, "offering ~p", [Item]),
    keep_state, D#dataown_items = add(Item, OwnItems);
negotiate(cast, retract_offer, Item, D = #dataown_items = OwnItems) ->
    undo_offer(D#data.other, Item),
    notice(D, "cancelling offer on ~p", [Item]),
    keep_state, D#dataown_items = remove(Item, OwnItems);
negotiate(cast, do_offer, Item, D = #dataother_items = OtherItems) ->
    notice(D, "other player offering ~p", [Item]),
    keep_state, D#dataother_items = add(Item, OtherItems);
negotiate(cast, undo_offer, Item, D = #dataother_items = OtherItems) ->
    notice(D, "Other player cancelling offer on ~p", [Item]),
    keep_state, D#dataother_items = remove(Item, OtherItems);
negotiate(cast, are_you_ready, D = #dataother = OtherPid) ->
    io:format("Other user ready to trade~n"),
    notice(D, "Other user ready to transfer goods:~nYou get ~p, the other side gets ~p", [D#data.other_items, D#data.own_items]),
    not_yet(OtherPid),
    keep_state_and_data;
negotiate(call, From, ready, D = #dataother = OtherPid) ->
    are_you_ready(OtherPid),
    notice(D, "asking if ready, waiting", []),
    next_state, wait, D#datafrom = From;
negotiate(EventType, Event, Data) ->
    unexpected(EventType, Event, Data, negotiate),
    keep_state_and_data.

wait(cast, do_offer, Item, D = #dataother_items = OtherItems) ->
    gen_statem:reply(D#data.from, offer_changed),
    notice(D, "Other side offering ~p", [Item]),
    next_state, negotiate, D#dataother_items = add(Item, OtherItems);
wait(cast, undo_offer, Item, D = #dataother_items = OtherItems) ->
    gen_statem:reply(D#data.from, offer_changed),
    notice(D, "other side cancelling offer ~p", [Item]),
    next_state, negotiate, D#dataother_items = remove(Item, OtherItems);
wait(cast, are_you_ready, D = #data) ->
    am_ready(D#data.other),
    notice(D, "asked if ready, and I am. Waiting for same reply", []),
    keep_state_and_data;
wait(cast, not_yet, D = #data) ->
    notice(D, "Other not ready yet", []),
    keep_state_and_data;
wait(cast, ready, D = #data) ->
    am_ready(D#data.other),
    ack_trans(D#data.other),
    gen_statem:reply(D#data.from, ok),
    notice(D, "other side is ready. Moving to ready state", []),
    next_state, ready, D;
wait(_, Event, _) ->
    unexpected(Event, wait),
    keep_state_and_data.

ready(cast, ack, D = #data) ->
    case priority(self(), D#data.other) of
        true ->
            try
                notice(D, "asking for commit", []),
                ready_commit = ask_commit(D#data.other),
                notice(D, "ordering commit", []),
                ok = do_commit(D#data.other),
                notice(D, "committing...", []),
                commit(D),
                % Sus
                stop, normal, D
            catch Class:Reason ->
                notice(D, "commit failed", []),
                % Sus
                stop, Class, Reason, D
            end;
        false ->
            keep_state_and_data
    end;
ready(call, From, ask_commit, D) ->
    notice(D, "replying to ask commit", []),
    % Sus
    keep_state_and_data, reply, From, ready_commit;
ready(call, _, do_commit, D) ->
    notice(D, "committing...", []),
    commit(D),
    % Sus
    stop, normal, ok, D;
ready(_, Event, _) ->
    unexpected(Event, ready),
    keep_state_and_data.

% Private functions
add (Item, Items) -> [Item | Items].

remove(Item, Items) -> Items -- [Item].

notice(#dataname = N, Str, Args) -> io:format("~s: " ++ Str ++ "~n", [N | Args]).

unexpected(Msg, State) -> io:format("~p received unknown event ~p while in state ~p~n", [self(), Msg, State]).

priority(OwnPid, OtherPid) when OwnPid > OtherPid -> true;
priority(OwnPid, OtherPid) when OwnPid < OtherPid -> false.

commit(D = #data) -> io:format("Transaction completed for ~s. Items sent are:~n~p,~n received are:~n~p.~nThis operation should have some atomic save in a database.~n", [D#data.name, D#data.own_items, D#data.other_items]).

【讨论】:

现在工作正常,非常感谢您的帮助。唯一仍然不起作用的是“% sus” cmets 下的返回,它们会产生崩溃。我试图推断如何将它们与文档一起使用,但我可能弄错了。请问你有什么办法改正吗?

以上是关于将 Learn You Some Erlang 教程从 gen_fsm 转换为 gen_statem的主要内容,如果未能解决你的问题,请参考以下文章

It is only that time that you take some action and take some risk that something occurs!

Vue:You may use special comments to disable some warnings.

Vue:You may use special comments to disable some warnings.

vue 项目报错 You may use special comments to disable some warnings.

读 Learn You a Haskell for Great Good!

Elixir重命名并包装Erlang模块?