diff options
author | Trygve Laugstøl <trygvis@inamo.no> | 2024-02-23 07:08:18 +0100 |
---|---|---|
committer | Trygve Laugstøl <trygvis@inamo.no> | 2024-02-23 07:08:18 +0100 |
commit | 5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e (patch) | |
tree | 982ca2e7f9ac4e8c350dfb5c4f60bcfdfff5afaf /learn-you-some-erlang/trade | |
parent | 05ae56e5e89abf2993f84e6d52b250131f247c35 (diff) | |
download | erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.gz erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.bz2 erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.xz erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.zip |
wip
Diffstat (limited to 'learn-you-some-erlang/trade')
-rw-r--r-- | learn-you-some-erlang/trade/trade_calls.erl | 144 | ||||
-rw-r--r-- | learn-you-some-erlang/trade/trade_fsm.erl | 359 |
2 files changed, 503 insertions, 0 deletions
diff --git a/learn-you-some-erlang/trade/trade_calls.erl b/learn-you-some-erlang/trade/trade_calls.erl new file mode 100644 index 0000000..e72c891 --- /dev/null +++ b/learn-you-some-erlang/trade/trade_calls.erl @@ -0,0 +1,144 @@ +-module(trade_calls). +-compile(export_all). + +%% 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_fsm:start_link("Carl"), + Parent ! Pid, + io:format("Spawned Carl: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(800), + trade_fsm:accept_trade(Pid), + timer:sleep(400), + io:format("~p~n",[trade_fsm:ready(Pid)]), + timer:sleep(1000), + trade_fsm:make_offer(Pid, "horse"), + trade_fsm:make_offer(Pid, "sword"), + timer:sleep(1000), + io:format("a synchronizing~n"), + sync2(), + trade_fsm:ready(Pid), + timer:sleep(200), + trade_fsm:ready(Pid), + timer:sleep(1000). + +b(PidA, PidCliA) -> + {ok, Pid} = trade_fsm:start_link("Jim"), + io:format("Spawned Jim: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(500), + trade_fsm:trade(Pid, PidA), + trade_fsm:make_offer(Pid, "boots"), + timer:sleep(200), + trade_fsm:retract_offer(Pid, "boots"), + timer:sleep(500), + trade_fsm:make_offer(Pid, "shotgun"), + timer:sleep(1000), + io:format("b synchronizing~n"), + sync1(PidCliA), + trade_fsm:make_offer(Pid, "horse"), %% race condition! + trade_fsm: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_fsm:start_link("Marc"), + Parent ! Pid, + receive PidD -> PidD end, + io:format("Spawned Marc: ~p~n", [Pid]), + %sys:trace(Pid, true), + sync2(), + trade_fsm:trade(Pid, PidD), + %% no need to accept_trade thanks to the race condition + timer:sleep(200), + trade_fsm:retract_offer(Pid, "car"), + trade_fsm:make_offer(Pid, "horse"), + timer:sleep(600), + trade_fsm:cancel(Pid), + timer:sleep(1000). + +d(Parent, PidC, PidCliC) -> + {ok, Pid} = trade_fsm:start_link("Pete"), + Parent ! Pid, + io:format("Spawned Jim: ~p~n", [Pid]), + %sys:trace(Pid,true), + sync1(PidCliC), + trade_fsm:trade(Pid, PidC), + %% no need to accept_trade thanks to the race condition + timer:sleep(200), + trade_fsm:retract_offer(Pid, "car"), + trade_fsm:make_offer(Pid, "manatee"), + timer:sleep(100), + trade_fsm: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_fsm:start_link("Carl"), + Parent ! Pid, + io:format("Spawned Carl: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(800), + trade_fsm:accept_trade(Pid), + timer:sleep(400), + io:format("~p~n",[trade_fsm:ready(Pid)]), + timer:sleep(1000), + trade_fsm:make_offer(Pid, "horse"), + trade_fsm:make_offer(Pid, "sword"), + timer:sleep(1000), + io:format("a synchronizing~n"), + sync2(), + trade_fsm:ready(Pid), + timer:sleep(200), + trade_fsm:ready(Pid), + timer:sleep(1000). + +f(PidE, PidCliE) -> + {ok, Pid} = trade_fsm:start_link("Jim"), + io:format("Spawned Jim: ~p~n", [Pid]), + %sys:trace(Pid,true), + timer:sleep(500), + trade_fsm:trade(Pid, PidE), + trade_fsm:make_offer(Pid, "boots"), + timer:sleep(200), + trade_fsm:retract_offer(Pid, "boots"), + timer:sleep(500), + trade_fsm:make_offer(Pid, "shotgun"), + timer:sleep(1000), + io:format("b synchronizing~n"), + sync1(PidCliE), + trade_fsm:make_offer(Pid, "horse"), + timer:sleep(200), + trade_fsm:ready(Pid), + timer:sleep(1000). + +%%% Utils +sync1(Pid) -> + Pid ! self(), + receive ack -> ok end. + +sync2() -> + receive + From -> From ! ack + end. diff --git a/learn-you-some-erlang/trade/trade_fsm.erl b/learn-you-some-erlang/trade/trade_fsm.erl new file mode 100644 index 0000000..fc66870 --- /dev/null +++ b/learn-you-some-erlang/trade/trade_fsm.erl @@ -0,0 +1,359 @@ +-module(trade_fsm). +-behaviour(gen_fsm). + +%% public API +-export([start/1, start_link/1, trade/2, accept_trade/1, + make_offer/2, retract_offer/2, ready/1, cancel/1]). +%% gen_fsm callbacks +-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, + terminate/3, code_change/4, + % custom state names + idle/2, idle/3, idle_wait/2, idle_wait/3, negotiate/2, + negotiate/3, wait/2, ready/2, ready/3]). + +-record(state, {name="", + other, + ownitems=[], + otheritems=[], + monitor, + from}). + +%%% PUBLIC API +start(Name) -> + gen_fsm:start(?MODULE, [Name], []). + +start_link(Name) -> + gen_fsm:start_link(?MODULE, [Name], []). + +%% ask for a begin session. Returns when/if the other accepts +trade(OwnPid, OtherPid) -> + gen_fsm:sync_send_event(OwnPid, {negotiate, OtherPid}, 30000). + +%% Accept someone's trade offer. +accept_trade(OwnPid) -> + gen_fsm:sync_send_event(OwnPid, accept_negotiate). + +%% Send an item on the table to be traded +make_offer(OwnPid, Item) -> + gen_fsm:send_event(OwnPid, {make_offer, Item}). + +%% Cancel trade offer +retract_offer(OwnPid, Item) -> + gen_fsm:send_event(OwnPid, {retract_offer, Item}). + +%% Mention that you're ready for a trade. When the other +%% player also declares being ready, the trade is done +ready(OwnPid) -> + gen_fsm:sync_send_event(OwnPid, ready, infinity). + +%% Cancel the transaction. +cancel(OwnPid) -> + gen_fsm:sync_send_all_state_event(OwnPid, cancel). + +%%% CLIENT-TO-CLIENT API +%% These calls are only listed for the gen_fsm to call +%% among themselves +%% All calls are asynchronous to avoid deadlocks + +%% Ask the other FSM for a trade session +ask_negotiate(OtherPid, OwnPid) -> + gen_fsm:send_event(OtherPid, {ask_negotiate, OwnPid}). + +%% Forward the client message accepting the transaction +accept_negotiate(OtherPid, OwnPid) -> + gen_fsm:send_event(OtherPid, {accept_negotiate, OwnPid}). + +%% forward a client's offer +do_offer(OtherPid, Item) -> + gen_fsm:send_event(OtherPid, {do_offer, Item}). + +%% forward a client's offer cancellation +undo_offer(OtherPid, Item) -> + gen_fsm:send_event(OtherPid, {undo_offer, Item}). + +%% Ask the other side if he's ready to trade. +are_you_ready(OtherPid) -> + gen_fsm:send_event(OtherPid, are_you_ready). + +%% Reply that the side is not ready to trade +%% i.e. is not in 'wait' state. +not_yet(OtherPid) -> + gen_fsm:send_event(OtherPid, not_yet). + +%% Tells the other fsm that the user is currently waiting +%% for the ready state. State should transition to 'ready' +am_ready(OtherPid) -> + gen_fsm:send_event(OtherPid, 'ready!'). + +%% Acknowledge that the fsm is in a ready state. +ack_trans(OtherPid) -> + gen_fsm:send_event(OtherPid, ack). + +%% ask if ready to commit +ask_commit(OtherPid) -> + gen_fsm:sync_send_event(OtherPid, ask_commit). + +%% begin the synchronous commit +do_commit(OtherPid) -> + gen_fsm:sync_send_event(OtherPid, do_commit). + +%% Make the other FSM aware that your client cancelled the trade +notify_cancel(OtherPid) -> + gen_fsm:send_all_state_event(OtherPid, cancel). + +%%% GEN_FSM API +init(Name) -> + {ok, idle, #state{name=Name}}. + + +%% idle state is the state before any trade is done. +%% The other player asks for a negotiation. We basically +%% only wait for our own user to accept the trade, +%% and store the other's Pid for future uses +idle({ask_negotiate, OtherPid}, S=#state{}) -> + Ref = monitor(process, OtherPid), + notice(S, "~p asked for a trade negotiation", [OtherPid]), + {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref}}; +idle(Event, Data) -> + unexpected(Event, idle), + {next_state, idle, Data}. + +%% trade call coming from the user. Forward to the other side, +%% forward it and store the other's Pid +idle({negotiate, OtherPid}, From, S=#state{}) -> + ask_negotiate(OtherPid, self()), + notice(S, "asking user ~p for a trade", [OtherPid]), + Ref = monitor(process, OtherPid), + {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref, from=From}}; +idle(Event, _From, Data) -> + unexpected(Event, idle), + {next_state, idle, Data}. + +%% idle_wait allows to expect replies from the other side and +%% start negotiating for items + +%% the other side asked for a negotiation while we asked for it too. +%% this means both definitely agree to the idea of doing a trade. +%% Both sides can assume the other feels the same! +idle_wait({ask_negotiate, OtherPid}, S=#state{other=OtherPid}) -> + gen_fsm:reply(S#state.from, ok), + notice(S, "starting negotiation", []), + {next_state, negotiate, S}; +%% The other side has accepted our offer. Move to negotiate state +idle_wait({accept_negotiate, OtherPid}, S=#state{other=OtherPid}) -> + gen_fsm:reply(S#state.from, ok), + notice(S, "starting negotiation", []), + {next_state, negotiate, S}; +%% different call from someone else. Not supported! Let it die. +idle_wait(Event, Data) -> + unexpected(Event, idle_wait), + {next_state, idle_wait, Data}. + +%% Our own client has decided to accept the transaction. +%% Make the other FSM aware of it and move to negotiate state. +idle_wait(accept_negotiate, _From, S=#state{other=OtherPid}) -> + accept_negotiate(OtherPid, self()), + notice(S, "accepting negotiation", []), + {reply, ok, negotiate, S}; +idle_wait(Event, _From, Data) -> + unexpected(Event, idle_wait), + {next_state, idle_wait, Data}. + +%% own side offering an item +negotiate({make_offer, Item}, S=#state{ownitems=OwnItems}) -> + do_offer(S#state.other, Item), + notice(S, "offering ~p", [Item]), + {next_state, negotiate, S#state{ownitems=add(Item, OwnItems)}}; +%% Own side retracting an item offer +negotiate({retract_offer, Item}, S=#state{ownitems=OwnItems}) -> + undo_offer(S#state.other, Item), + notice(S, "cancelling offer on ~p", [Item]), + {next_state, negotiate, S#state{ownitems=remove(Item, OwnItems)}}; +%% other side offering an item +negotiate({do_offer, Item}, S=#state{otheritems=OtherItems}) -> + notice(S, "other player offering ~p", [Item]), + {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}}; +%% other side retracting an item offer +negotiate({undo_offer, Item}, S=#state{otheritems=OtherItems}) -> + notice(S, "Other player cancelling offer on ~p", [Item]), + {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}}; +%% Other side has declared itself ready. Our own FSM should tell it to +%% wait (with not_yet/1). +negotiate(are_you_ready, S=#state{other=OtherPid}) -> + io:format("Other user ready to trade.~n"), + notice(S, + "Other user ready to transfer goods:~n" + "You get ~p, The other side gets ~p", + [S#state.otheritems, S#state.ownitems]), + not_yet(OtherPid), + {next_state, negotiate, S}; +negotiate(Event, Data) -> + unexpected(Event, negotiate), + {next_state, negotiate, Data}. + +%% own user mentioning he is ready. Next state should be wait +%% and we add the 'from' to the state so we can reply to the +%% user once ready. +negotiate(ready, From, S = #state{other=OtherPid}) -> + are_you_ready(OtherPid), + notice(S, "asking if ready, waiting", []), + {next_state, wait, S#state{from=From}}; +negotiate(Event, _From, S) -> + unexpected(Event, negotiate), + {next_state, negotiate, S}. + +%% other side offering an item. Don't forget our client is still +%% waiting for a reply, so let's tell them the trade state changed +%% and move back to the negotiate state +wait({do_offer, Item}, S=#state{otheritems=OtherItems}) -> + gen_fsm:reply(S#state.from, offer_changed), + notice(S, "other side offering ~p", [Item]), + {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}}; +%% other side cancelling an item offer. Don't forget our client is still +%% waiting for a reply, so let's tell them the trade state changed +%% and move back to the negotiate state +wait({undo_offer, Item}, S=#state{otheritems=OtherItems}) -> + gen_fsm:reply(S#state.from, offer_changed), + notice(S, "Other side cancelling offer of ~p", [Item]), + {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}}; +%% The other client falls in ready state and asks us about it. +%% However, the other client could have moved out of wait state already. +%% Because of this, we send that we indeed are 'ready!' and hope for them +%% to do the same. +wait(are_you_ready, S=#state{}) -> + am_ready(S#state.other), + notice(S, "asked if ready, and I am. Waiting for same reply", []), + {next_state, wait, S}; +%% The other client is not ready to trade yet. We keep waiting +%% and won't reply to our own client yet. +wait(not_yet, S = #state{}) -> + notice(S, "Other not ready yet", []), + {next_state, wait, S}; +%% The other client was waiting for us! Let's reply to ours and +%% send the ack message for the commit initiation on the other end. +%% We can't go back after this. +wait('ready!', S=#state{}) -> + am_ready(S#state.other), + ack_trans(S#state.other), + gen_fsm:reply(S#state.from, ok), + notice(S, "other side is ready. Moving to ready state", []), + {next_state, ready, S}; +wait(Event, Data) -> + unexpected(Event, wait), + {next_state, wait, Data}. + +%% Ready state with the acknowledgement message coming from the +%% other side. We determine if we should begin the synchronous +%% commit or if the other side should. +%% A successful commit (if we initiated it) could be done +%% in the terminate function or any other before. +ready(ack, S=#state{}) -> + case priority(self(), S#state.other) of + true -> + try + notice(S, "asking for commit", []), + ready_commit = ask_commit(S#state.other), + notice(S, "ordering commit", []), + ok = do_commit(S#state.other), + notice(S, "committing...", []), + commit(S), + {stop, normal, S} + catch Class:Reason -> + %% abort! Either ready_commit or do_commit failed + notice(S, "commit failed", []), + {stop, {Class, Reason}, S} + end; + false -> + {next_state, ready, S} + end; +ready(Event, Data) -> + unexpected(Event, ready), + {next_state, ready, Data}. + +%% We weren't the ones to initiate the commit. +%% Let's reply to the other side to say we're doing our part +%% and terminate. +ready(ask_commit, _From, S) -> + notice(S, "replying to ask_commit", []), + {reply, ready_commit, ready, S}; +ready(do_commit, _From, S) -> + notice(S, "committing...", []), + commit(S), + {stop, normal, ok, S}; +ready(Event, _From, Data) -> + unexpected(Event, ready), + {next_state, ready, Data}. + +%% This cancel event has been sent by the other player +%% stop whatever we're doing and shut down! +handle_event(cancel, _StateName, S=#state{}) -> + notice(S, "received cancel event", []), + {stop, other_cancelled, S}; +handle_event(Event, StateName, Data) -> + unexpected(Event, StateName), + {next_state, StateName, Data}. + +%% This cancel event comes from the client. We must warn the other +%% player that we have a quitter! +handle_sync_event(cancel, _From, _StateName, S = #state{}) -> + notify_cancel(S#state.other), + notice(S, "cancelling trade, sending cancel event", []), + {stop, cancelled, ok, S}; +%% Note: DO NOT reply to unexpected calls. Let the call-maker crash! +handle_sync_event(Event, _From, StateName, Data) -> + unexpected(Event, StateName), + {next_state, StateName, Data}. + +%% The other player's FSM has gone down. We have +%% to abort the trade. +handle_info({'DOWN', Ref, process, Pid, Reason}, _, S=#state{other=Pid, monitor=Ref}) -> + notice(S, "Other side dead", []), + {stop, {other_down, Reason}, S}; +handle_info(Info, StateName, Data) -> + unexpected(Info, StateName), + {next_state, StateName, Data}. + +code_change(_OldVsn, StateName, Data, _Extra) -> + {ok, StateName, Data}. + +%% Transaction completed. +terminate(normal, ready, S=#state{}) -> + notice(S, "FSM leaving.", []); +terminate(_Reason, _StateName, _StateData) -> + ok. + +%%% PRIVATE FUNCTIONS + +%% adds an item to an item list +add(Item, Items) -> + [Item | Items]. + +%% remove an item from an item list +remove(Item, Items) -> + Items -- [Item]. + +%% Send players a notice. This could be messages to their clients +%% but for our purposes, outputting to the shell is enough. +notice(#state{name=N}, Str, Args) -> + io:format("~s: "++Str++"~n", [N|Args]). + +%% Unexpected allows to log unexpected messages +unexpected(Msg, State) -> + io:format("~p received unknown event ~p while in state ~p~n", + [self(), Msg, State]). + +%% This function allows two processes to make a synchronous call to each +%% other by electing one Pid to do it. Both processes call it and it +%% tells them whether they should initiate the call or not. +%% This is done by knowing that Erlang will alwys sort Pids in an +%% absolute manner depending on when and where they were spawned. +priority(OwnPid, OtherPid) when OwnPid > OtherPid -> true; +priority(OwnPid, OtherPid) when OwnPid < OtherPid -> false. + +commit(S = #state{}) -> + io:format("Transaction completed for ~s. " + "Items sent are:~n~p,~n received are:~n~p.~n" + "This operation should have some atomic save " + "in a database.~n", + [S#state.name, S#state.ownitems, S#state.otheritems]). + |