aboutsummaryrefslogtreecommitdiff
path: root/learn-you-some-erlang/trade
diff options
context:
space:
mode:
authorTrygve Laugstøl <trygvis@inamo.no>2024-02-23 07:08:18 +0100
committerTrygve Laugstøl <trygvis@inamo.no>2024-02-23 07:08:18 +0100
commit5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e (patch)
tree982ca2e7f9ac4e8c350dfb5c4f60bcfdfff5afaf /learn-you-some-erlang/trade
parent05ae56e5e89abf2993f84e6d52b250131f247c35 (diff)
downloaderlang-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.erl144
-rw-r--r--learn-you-some-erlang/trade/trade_fsm.erl359
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]).
+