Advertisement
Guest User

trade_fsm

a guest
Jan 9th, 2019
157
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Erlang 15.72 KB | None | 0 0
  1. -module(trade_fsm).
  2. -behaviour(gen_fsm).
  3.  
  4. %%______________________________________________________________________
  5. %% public API
  6. -export([start/1, start_link/1, trade/2, accept_trade/1,
  7.          make_final_offer/2,
  8.          make_offer/2, retract_offer/2, ready/1, cancel/1]).
  9. %% gen_fsm callbacks
  10. -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3,
  11.          terminate/3, code_change/4,
  12.          % custom state names
  13.          idle/2, idle/3, idle_wait/2, idle_wait/3, negotiate/2,
  14.          negotiate/3, final_wait/2, wait/2, ready/2, ready/3]).
  15. -record(state, {name="",
  16.                 other,
  17.                 ownitems=[],
  18.                 otheritems=[],
  19.                 monitor,
  20.                 from}).
  21.  
  22. %%% PUBLIC API
  23. start(Name) ->
  24.     gen_fsm:start(?MODULE, [Name], []).
  25.  
  26. start_link(Name) ->
  27.     gen_fsm:start_link(?MODULE, [Name], []).
  28.  
  29. %% ask for a begin session. Returns when/if the other accepts
  30. trade(OwnPid, OtherPid) ->
  31.     gen_fsm:sync_send_event(OwnPid, {negotiate, OtherPid}, 30000).
  32.  
  33. %% Accept someone's trade offer.
  34. accept_trade(OwnPid) ->
  35.     gen_fsm:sync_send_event(OwnPid, accept_negotiate).
  36.  
  37. %% Send an item on the table to be traded
  38. make_offer(OwnPid, Item) ->
  39.     gen_fsm:send_event(OwnPid, {make_offer, Item}).
  40.  
  41. %%______________________________________________________________________
  42. make_final_offer(OwnPid, Item) ->
  43.     gen_fsm:send_event(OwnPid, {make_final_offer, Item}).
  44.    
  45.  
  46. %% Cancel trade offer
  47. retract_offer(OwnPid, Item) ->
  48.     gen_fsm:send_event(OwnPid, {retract_offer, Item}).
  49.  
  50. %% Mention that you're ready for a trade. When the other
  51. %% player also declares being ready, the trade is done
  52. ready(OwnPid) ->
  53.     gen_fsm:sync_send_event(OwnPid, ready, infinity).
  54.  
  55. %% Cancel the transaction.
  56. cancel(OwnPid) ->
  57.     gen_fsm:sync_send_all_state_event(OwnPid, cancel).
  58.  
  59. %%% CLIENT-TO-CLIENT API
  60. %% These calls are only listed for the gen_fsm to call
  61. %% among themselves
  62. %% All calls are asynchronous to avoid deadlocks
  63.  
  64. %% Ask the other FSM for a trade session
  65. ask_negotiate(OtherPid, OwnPid) ->
  66.     gen_fsm:send_event(OtherPid, {ask_negotiate, OwnPid}).
  67.  
  68. %% Forward the client message accepting the transaction
  69. accept_negotiate(OtherPid, OwnPid) ->
  70.     gen_fsm:send_event(OtherPid, {accept_negotiate, OwnPid}).
  71.  
  72. %% forward a client's offer
  73. do_offer(OtherPid, Item) ->
  74.     gen_fsm:send_event(OtherPid, {do_offer, Item}).
  75.  
  76. %%______________________________________________________________________
  77. exec_final_offer(OtherPid, Item) ->
  78.     gen_fsm:send_event(OtherPid, {do_final_offer, Item}).
  79.    
  80.  
  81. %% forward a client's offer cancellation
  82. undo_offer(OtherPid, Item) ->
  83.     gen_fsm:send_event(OtherPid, {undo_offer, Item}).
  84.  
  85. %% Ask the other side if he's ready to trade.
  86. are_you_ready(OtherPid) ->
  87.     gen_fsm:send_event(OtherPid, are_you_ready).
  88.  
  89. %% Reply that the side is not ready to trade
  90. %% i.e. is not in 'wait' state.
  91. not_yet(OtherPid) ->
  92.     gen_fsm:send_event(OtherPid, not_yet).
  93.  
  94. %% Tells the other fsm that the user is currently waiting
  95. %% for the ready state. State should transition to 'ready'
  96. am_ready(OtherPid) ->
  97.     gen_fsm:send_event(OtherPid, 'ready!').
  98.  
  99. %% Acknowledge that the fsm is in a ready state.
  100. ack_trans(OtherPid) ->
  101.     gen_fsm:send_event(OtherPid, ack).
  102.  
  103. %% ask if ready to commit
  104. ask_commit(OtherPid) ->
  105.     gen_fsm:sync_send_event(OtherPid, ask_commit).
  106.  
  107. %% begin the synchronous commit
  108. do_commit(OtherPid) ->
  109.     gen_fsm:sync_send_event(OtherPid, do_commit).
  110.  
  111. %% Make the other FSM aware that your client cancelled the trade
  112. notify_cancel(OtherPid) ->
  113.     gen_fsm:send_all_state_event(OtherPid, cancel).
  114.  
  115. %%% GEN_FSM API
  116. init(Name) ->
  117.     {ok, idle, #state{name=Name}}.
  118.  
  119.  
  120. %% idle state is the state before any trade is done.
  121. %% The other player asks for a negotiation. We basically
  122. %% only wait for our own user to accept the trade,
  123. %% and store the other's Pid for future uses
  124. idle({ask_negotiate, OtherPid}, S=#state{}) ->
  125.     Ref = monitor(process, OtherPid),
  126.     notice(S, "~p asked for a trade negotiation", [OtherPid]),
  127.     {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref}};
  128. idle(Event, Data) ->
  129.     unexpected(Event, idle),
  130.     {next_state, idle, Data}.
  131.  
  132. %% trade call coming from the user. Forward to the other side,
  133. %% forward it and store the other's Pid
  134. idle({negotiate, OtherPid}, From, S=#state{}) ->
  135.     ask_negotiate(OtherPid, self()),
  136.     notice(S, "asking user ~p for a trade", [OtherPid]),
  137.     Ref = monitor(process, OtherPid),
  138.     {next_state, idle_wait, S#state{other=OtherPid, monitor=Ref, from=From}};
  139. idle(Event, _From, Data) ->
  140.     unexpected(Event, idle),
  141.     {next_state, idle, Data}.
  142.  
  143. %% idle_wait allows to expect replies from the other side and
  144. %% start negotiating for items
  145.  
  146. %% the other side asked for a negotiation while we asked for it too.
  147. %% this means both definitely agree to the idea of doing a trade.
  148. %% Both sides can assume the other feels the same!
  149. idle_wait({ask_negotiate, OtherPid}, S=#state{other=OtherPid}) ->
  150.     gen_fsm:reply(S#state.from, ok),
  151.     notice(S, "starting negotiation", []),
  152.     {next_state, negotiate, S};
  153. %% The other side has accepted our offer. Move to negotiate state
  154. idle_wait({accept_negotiate, OtherPid}, S=#state{other=OtherPid}) ->
  155.     gen_fsm:reply(S#state.from, ok),
  156.     notice(S, "starting negotiation", []),
  157.     {next_state, negotiate, S};
  158. %% different call from someone else. Not supported! Let it die.
  159. idle_wait(Event, Data) ->
  160.     unexpected(Event, idle_wait),
  161.     {next_state, idle_wait, Data}.
  162.  
  163. %% Our own client has decided to accept the transaction.
  164. %% Make the other FSM aware of it and move to negotiate state.
  165. idle_wait(accept_negotiate, _From, S=#state{other=OtherPid}) ->
  166.     accept_negotiate(OtherPid, self()),
  167.     notice(S, "accepting negotiation", []),
  168.     {reply, ok, negotiate, S};
  169. idle_wait(Event, _From, Data) ->
  170.     unexpected(Event, idle_wait),
  171.     {next_state, idle_wait, Data}.
  172.  
  173. %% own side offering an item
  174. negotiate({make_offer, Item}, S=#state{ownitems=OwnItems}) ->
  175.     do_offer(S#state.other, Item),
  176.     notice(S, "offering ~p", [Item]),
  177.     {next_state, negotiate, S#state{ownitems=add(Item, OwnItems)}};
  178.  
  179. %%______________________________________________________________________
  180. negotiate({make_final_offer, Item}, S=#state{ownitems=OwnItems}) ->
  181.     do_final_offer(S#state.other, Item),
  182.     notice(S, "final offering ~p", [Item]),
  183.     are_you_ready(S#state.other),
  184.     {next_state, final_wait, S#state{ownitems=add(Item, OwnItems)}, 5000};
  185.    
  186.    
  187. %% Own side retracting an item offer
  188. negotiate({retract_offer, Item}, S=#state{ownitems=OwnItems}) ->
  189.     undo_offer(S#state.other, Item),
  190.     notice(S, "cancelling offer on ~p", [Item]),
  191.     {next_state, negotiate, S#state{ownitems=remove(Item, OwnItems)}};
  192. %% other side offering an item
  193. negotiate({do_offer, Item}, S=#state{otheritems=OtherItems}) ->
  194.     notice(S, "other player offering ~p", [Item]),
  195.     {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}};
  196.    
  197.    
  198. %%______________________________________________________________________
  199. negotiate({do_final_offer, Item}, S=#state{otheritems=OtherItems}) ->
  200.     notice(S, "other player final offering ~p", [Item]),
  201.     {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}};
  202.    
  203.    
  204. %% other side retracting an item offer
  205. negotiate({undo_offer, Item}, S=#state{otheritems=OtherItems}) ->
  206.     notice(S, "Other player cancelling offer on ~p", [Item]),
  207.     {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}};
  208. %% Other side has declared itself ready. Our own FSM should tell it to
  209. %% wait (with not_yet/1).
  210. negotiate(are_you_ready, S=#state{other=OtherPid}) ->
  211.     io:format("Other user ready to trade.~n"),
  212.     notice(S,
  213.            "Other user ready to transfer goods:~n"
  214.            "You get ~p, The other side gets ~p",
  215.            [S#state.otheritems, S#state.ownitems]),
  216.     not_yet(OtherPid),
  217.     {next_state, negotiate, S};
  218. negotiate(Event, Data) ->
  219.     unexpected(Event, negotiate),
  220.     {next_state, negotiate, Data}.
  221.  
  222. %% own user mentioning he is ready. Next state should be wait
  223. %% and we add the 'from' to the state so we can reply to the
  224. %% user once ready.
  225. negotiate(ready, From, S = #state{other=OtherPid}) ->
  226.     are_you_ready(OtherPid),
  227.     notice(S, "asking if ready, waiting", []),
  228.     {next_state, wait, S#state{from=From}};
  229. negotiate(Event, _From, S) ->
  230.     unexpected(Event, negotiate),
  231.     {next_state, negotiate, S}.
  232.  
  233. %% other side offering an item. Don't forget our client is still
  234. %% waiting for a reply, so let's tell them the trade state changed
  235. %% and move back to the negotiate state
  236. wait({do_offer, Item}, S=#state{otheritems=OtherItems}) ->
  237.     gen_fsm:reply(S#state.from, offer_changed),
  238.     notice(S, "other side offering ~p", [Item]),
  239.     {next_state, negotiate, S#state{otheritems=add(Item, OtherItems)}};
  240. %% other side cancelling an item offer. Don't forget our client is still
  241. %% waiting for a reply, so let's tell them the trade state changed
  242. %% and move back to the negotiate state
  243. wait({undo_offer, Item}, S=#state{otheritems=OtherItems}) ->
  244.     gen_fsm:reply(S#state.from, offer_changed),
  245.     notice(S, "Other side cancelling offer of ~p", [Item]),
  246.     {next_state, negotiate, S#state{otheritems=remove(Item, OtherItems)}};
  247. %% The other client falls in ready state and asks us about it.
  248. %% However, the other client could have moved out of wait state already.
  249. %% Because of this, we send that we indeed are 'ready!' and hope for them
  250. %% to do the same.
  251. wait(are_you_ready, S=#state{}) ->
  252.     am_ready(S#state.other),
  253.     notice(S, "asked if ready, and I am. Waiting for same reply", []),
  254.     {next_state, wait, S};
  255. %% The other client is not ready to trade yet. We keep waiting
  256. %% and won't reply to our own client yet.
  257. wait(not_yet, S = #state{}) ->
  258.     notice(S, "Other not ready yet", []),
  259.     {next_state, wait, S};
  260. %% The other client was waiting for us! Let's reply to ours and
  261. %% send the ack message for the commit initiation on the other end.
  262. %% We can't go back after this.
  263. wait('ready!', S=#state{}) ->
  264.     am_ready(S#state.other),
  265.     ack_trans(S#state.other),
  266.     gen_fsm:reply(S#state.from, ok),
  267.     notice(S, "other side is ready. Moving to ready state", []),
  268.     {next_state, ready, S};
  269. wait(Event, Data) ->
  270.     unexpected(Event, wait),
  271.     {next_state, wait, Data}.
  272.  
  273. %%______________________________________________________________________
  274. final_wait({do_offer, Item}, S=#state{otheritems=OtherItems}) ->
  275.     notify_cancel(S#state.other),
  276.     notice(S, "Other player has changed his offer. Canceling...", []),
  277.     {stop, normal, ok, S};
  278. final_wait({undo_offer, Item}, S=#state{otheritems=OtherItems}) ->
  279.     notify_cancel(S#state.other),
  280.     notice(S, "Other player is cancelling offer of ~p Terminating", [Item]),
  281.     {stop, normal, ok, S};
  282. final_wait(are_you_ready, S=#state{}) ->
  283.     am_ready(S#state.other),
  284.     notice(S, "asked if ready, and I am. Waiting for same reply", []),
  285.     {next_state, final_wait, S};
  286. final_wait(not_yet, S = #state{}) ->
  287.     notice(S, "Other player is not ready yet", []),
  288.     {next_state, final_wait, S, 5000};
  289. final_wait('ready!', S=#state{}) ->
  290.     am_ready(S#state.other),
  291.     ack_trans(S#state.other),
  292.     gen_fsm:reply(S#state.from, ok),
  293.     notice(S, "Other player is ready. Changing state to ready.", []),
  294.     {next_state, ready, S};
  295. final_wait(timeout, S=#state{}) ->
  296.     notify_cancel(S#state.other),
  297.     notice(S, "Timeout, canceling...", []),
  298.     {stop, cancelled, ok, S};
  299. final_wait(Event, Data) ->
  300.     unexpected(Event, wait),
  301.     {next_state, final_wait, Data}.
  302.    
  303.  
  304. %% Ready state with the acknowledgement message coming from the
  305. %% other side. We determine if we should begin the synchronous
  306. %% commit or if the other side should.
  307. %% A successful commit (if we initiated it) could be done
  308. %% in the terminate function or any other before.
  309. ready(ack, S=#state{}) ->
  310.     case priority(self(), S#state.other) of
  311.         true ->
  312.             try
  313.                 notice(S, "asking for commit", []),
  314.                 ready_commit = ask_commit(S#state.other),
  315.                 notice(S, "ordering commit", []),
  316.                 ok = do_commit(S#state.other),
  317.                 notice(S, "committing...", []),
  318.                 commit(S),
  319.                 {stop, normal, S}
  320.             catch Class:Reason ->
  321.                 %% abort! Either ready_commit or do_commit failed
  322.                 notice(S, "commit failed", []),
  323.                 {stop, {Class, Reason}, S}
  324.             end;
  325.         false ->
  326.             {next_state, ready, S}
  327.     end;
  328. ready(Event, Data) ->
  329.     unexpected(Event, ready),
  330.     {next_state, ready, Data}.
  331.  
  332. %% We weren't the ones to initiate the commit.
  333. %% Let's reply to the other side to say we're doing our part
  334. %% and terminate.
  335. ready(ask_commit, _From, S) ->
  336.     notice(S, "replying to ask_commit", []),
  337.     {reply, ready_commit, ready, S};
  338. ready(do_commit, _From, S) ->
  339.     notice(S, "committing...", []),
  340.     commit(S),
  341.     {stop, normal, ok, S};
  342. ready(Event, _From, Data) ->
  343.     unexpected(Event, ready),
  344.     {next_state, ready, Data}.
  345.  
  346. %% This cancel event has been sent by the other player
  347. %% stop whatever we're doing and shut down!
  348. handle_event(cancel, _StateName, S=#state{}) ->
  349.     notice(S, "received cancel event", []),
  350.     {stop, other_cancelled, S};
  351. handle_event(Event, StateName, Data) ->
  352.     unexpected(Event, StateName),
  353.     {next_state, StateName, Data}.
  354.  
  355. %% This cancel event comes from the client. We must warn the other
  356. %% player that we have a quitter!
  357. handle_sync_event(cancel, _From, _StateName, S = #state{}) ->
  358.     notify_cancel(S#state.other),
  359.     notice(S, "cancelling trade, sending cancel event", []),
  360.     {stop, cancelled, ok, S};
  361. %% Note: DO NOT reply to unexpected calls. Let the call-maker crash!
  362. handle_sync_event(Event, _From, StateName, Data) ->
  363.     unexpected(Event, StateName),
  364.     {next_state, StateName, Data}.
  365.  
  366. %% The other player's FSM has gone down. We have
  367. %% to abort the trade.
  368. handle_info({'DOWN', Ref, process, Pid, Reason}, _, S=#state{other=Pid, monitor=Ref}) ->
  369.     notice(S, "Other side dead", []),
  370.     {stop, {other_down, Reason}, S};
  371. handle_info(Info, StateName, Data) ->
  372.     unexpected(Info, StateName),
  373.     {next_state, StateName, Data}.
  374.  
  375. code_change(_OldVsn, StateName, Data, _Extra) ->
  376.  {ok, StateName, Data}.
  377.  
  378. %% Transaction completed.
  379. terminate(normal, ready, S=#state{}) ->
  380.     notice(S, "FSM leaving.", []);
  381. terminate(_Reason, _StateName, _StateData) ->
  382.     ok.
  383.  
  384. %%% PRIVATE FUNCTIONS
  385.  
  386. %% adds an item to an item list
  387. add(Item, Items) ->
  388.     [Item | Items].
  389.  
  390. %% remove an item from an item list
  391. remove(Item, Items) ->
  392.     Items -- [Item].
  393.  
  394. %% Send players a notice. This could be messages to their clients
  395. %% but for our purposes, outputting to the shell is enough.
  396. notice(#state{name=N}, Str, Args) ->
  397.     io:format("~s: "++Str++"~n", [N|Args]).
  398.  
  399. %% Unexpected allows to log unexpected messages
  400. unexpected(Msg, State) ->
  401.     io:format("~p received unknown event ~p while in state ~p~n",
  402.               [self(), Msg, State]).
  403.  
  404. %% This function allows two processes to make a synchronous call to each
  405. %% other by electing one Pid to do it. Both processes call it and it
  406. %% tells them whether they should initiate the call or not.
  407. %% This is done by knowing that Erlang will alwys sort Pids in an
  408. %% absolute manner depending on when and where they were spawned.
  409. priority(OwnPid, OtherPid) when OwnPid > OtherPid -> true;
  410. priority(OwnPid, OtherPid) when OwnPid < OtherPid -> false.    
  411.  
  412. commit(S = #state{}) ->
  413.     io:format("Transaction completed for ~s. "
  414.               "Items sent are:~n~p,~n received are:~n~p.~n"
  415.               "This operation should have some atomic save "
  416.               "in a database.~n",
  417.               [S#state.name, S#state.ownitems, S#state.otheritems]).
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement