PostedbyMitchellonSeptember08,2008 An introduction to gen_fsm: ErlyBanks ATM This is the second article in my Erlang/OTP introduction series. If you havent yet, I recommend you read the first article which talks about gen_server and lays the foundation for our bank system before moving onto this article. That being said, if youre a quick learner, you can look at the completed server file and continue from there. The Scenario: We delivered ErlyBanks server to them and they were very pleased. But this is the 21st century so they also need a secure and easy to use ATM system, so theyve asked that we extend the server and also create their ATM backend. User accounts are to be protected with a four-digit PIN. From the ATM you should be able to log in to a previously existing account, and deposit or withdraw funds. There is no need to create a pretty user interface on top of the ATM backend, they have another team doing that. The Result: First, well extend the server software to hold a PIN for accounts, and add an authorization call to it. Then, well use gen_fsm to create the ATM backend. Well rely on the server to do all the data validation. As always, click read more to get started, if you havent already What is gen_fsm? gen_fsm is another Erlang/OTP behavior module, and is used for implementing finite state machines. I apologize in advance, since in this article state will be used to reference two different things: gen_fsm state - The state of a finite state machine, this is the current mode its in. It has nothing to do with the state you learned about with gen_server. state data - The state data of the server, this is the same as the state you learned about with gen_server. A bit confusing, I know, but I will do my best to only refer to them with the above terms. A gen_fsm starts in a state, and any calls/casts made to it are received by a special callback method which is the named the same as the state the gen_fsm module is in. Based on an action, the module can change states. A textbook example of a finite state machine is a locked door. The door starts in the state of locked. It requires four digits to be unlocked. After entering a single digit, the door stores it, but it needs more digits, and waits, still locked. After entering four digits, if they are correct, the door changes state to unlocked for a period of time. If the digits are incorrect, it remains locked and clears its memory. Youre probably getting an idea now of how were going to implement an ATM using gen_fsm now As I did with gen_server, here is the list of callbacks we need to implement for gen_fsm. Youll notice many of them are similar to gen_server: init/1 - Initializers the server. Almost identical to gen_server. StateName/2 - In this case, StateName actually will be replaced with a state name. This is called when a message is sent to the server; an action occurs on the finite state machine. This is an asynchronous callback. handle_event/3 - Similar to StateName/2, except that this is sent no matter what state youre in, when the client calls gen_fsm:send_all_state_event. Again, this is asynchronous. StateName/3 - Equivalent to StateName/2 except this is the synchronous version. The client waits for a response from the server before continuing. handle_sync_event/4 - Equivalent to handle_event/3 except this is the synchronous version. handle_info/3 - Equivalent to gen_servers handle_info. This receives all messages which werent sending using a standard gen_fsm command. This can include timeout messages, process exit messages, or any messages sent manually with the ! symbol to the server process. terminate/3 - Called when the server is terminating so you can clean up any resources. code_change/4 - Called when a real-time system upgrade of the server is occuring. We wont deal with this in this article, but it will be used extensively in a future article. The gen_fsm Skeleton Just like with gen_server, I start off every finite state machine with a generic skeleton. The gen_fsm skeleton can be viewed here. There should be nothing out of the ordinary there. The start_link method looks just like the gen_server one did. Save that skeleton as eb_atm.erl Now were ready to start going! Extending eb_server to do Account Authorization This is again one of those tasks which I feel comfortable allowing you to do on your own. The following are the changes we need to make: 1. Account creation should require a PIN now, which will be stored with the account, unencrypted. 2. Add an authorize/2 method, where the arguments are Name and PIN. The return values should be ok or {error,Reason}. Also, it would be a good idea to require the PIN, or some sort of authorization token with every request to deposit or withdraw money, but in order to save time, and since ErlyBank is fake (breaks my heart hah!), we will not do this. To be honest, this isnt really a quick-fix, but if youre learning Erlang now on your own, you must be pretty smart So I think you can do it! Be sure to test your changes before moving on, or at least compare them to the answer linked below. After implementing the above changes, your eb_server.erl should look like this. Note that the messages you send to the server may be different, and that is okay. Programmers think differently. The important thing is that the API outputs the same data, correctly. ATM Design Strategy Im going to quick a take coding break to explain the plan for this ATM system. Were going to implement it following the flow chart below: The three blue boxes represent the different states the server will be in. The arrows represent what actions are necessary to switch to those states. Initializing gen_fsm To start the ATM, we use the same start_link method as a gen_server. But initialization is a little bit different. init([]) -> {ok, unauthorized, nobody}.
The init/1 method of a gen_fsm is expected to return {ok,StateName,StateData}. StateName is the initial state of the server, and state data is the initial data of the server. In the case of ErlyBanks ATM, we start out in the unauthorized state, and the data is set to nobody. The state data will be the account name well be dealing with, so initially its nothing. Erlang has no null/nil/nothing data type, and a descriptive atom is usually used instead, like this one. Account Authorization We now need to implement the authorization API for the ATM. First, the API definition: authorize(Name, PIN) -> gen_fsm:sync_send_event(?SERVER, {authorize, Name, PIN}). The sync_send_event method is equivalent to the call method of gen_server. It sends the message, the second argument, to the current state of the server represented with the first argument. So now we have to write a handler for the message: unauthorized({authorize, Name, Pin}, _From, State) -> case eb_server:authorize(Name, Pin) of ok -> {reply, ok, authorized, Name}; {error, Reason} -> {reply, {error, Reason}, unauthorized, State} end; unauthorized(_Event, _From, State) -> Reply = {error, invalid_message}, {reply, Reply, unauthorized, State}.
This should read pretty logically as well. The function is named unauthorized because that is the state we expect the message to be received at. We then write a matcher to match the tuple {authorize,Name,Pin}. We then use the API methods exported by the server we wrote to authorize the user. If the account and PIN are valid, we send the response ok back to the client. The format of the response is {reply,Response, NewStateName,NewStateData}. Following that format, weve changed states to authorized and for the data we store the account name. If the account information was invalid, we respond with the error and reason and stay in the same state with the same state data. Finally, we implement another catch-all function at the end. You should do this throughout functional programming environments but it is especially important here, since different states might be receiving actions meant for other states. For example, what if, for some reason, someone attempts to deposit while were still unauthenticated? We need a catch all to send back an invalid message error. This is what we do here. Deposits Once were authorized, a user is going to either deposit or withdraw from his or her bank account. Were going to implement deposits using an asynchronous call to the server. Again, this is a bit insecure, since were not verifying the deposit was actually successful, but since this is a fake bank, and an ATM prototype at best, I am letting this slip. First, the API! %%-------------------------------------------------------------------- %% Function: deposit(Amount) -> ok %% Description: Deposits a certain amount in the currently authorized %% account. %%-------------------------------------------------------------------- deposit(Amount) -> gen_fsm:send_event(?SERVER, {deposit, Amount}).
Simple, and this time we use the send_event/2 method instead of sync_send_event. This sends an asynchronous call to the server. Now the handling part authorized({deposit, Amount}, State) -> eb_server:deposit(State, Amount), {next_state, thank_you, State, 5000}; authorized(_Event, State) -> {next_state, authorized, State}.
Again, very simple. The method actual just forwards the information to the eb_server deposit API we wrote, which will do all the validation. But there is something strange in the return value of the deposit method! Not only does the state change to thank_you, but there is this odd 5000 at the end there. That is actually a timeout. If no message is received in 5000 milliseconds (or 5 seconds), then a timeout message is sent to the current state. Which leads us to our next topic The Short Thank You! State Anyone (everyone?) who has gone to an ATM knows that after youre done banking, there is a short Thank You screen that shows up for a little while. And although this really shouldnt be implemented in the back end, I wanted to show off the timeout feature of gen_fsm. After 5000 milliseconds, or if any other message is received, I swap the state back over to unauthorized so the ATM can start over again for the next customer. Here is the code: thank_you(timeout, _State) -> {next_state, unauthorized, nobody}; thank_you(_Event, _State) -> {next_state, unauthorized, nobody}.
Note: The trained eye will note that both methods are equal, and its unnecessary to have the first matcher. This is true, and I just included the first matcher to be explicit about catching the timeout. Here is the complete eb_atm.erl up to this point. Withdrawals Again, I will be leaving withdrawals as an exercise to the reader. You can implement it any way youd like! Just make sure it actually does withdraw from the account Here is my eb_atm.erl after implementing withdrawals. Note that successful withdrawals transition to the thank_you state with the timeout in place. A Cancel-No-Matter-What Button One of my biggest gripes of many machines is that there isnt a big Cancel button that cancels everything you have been doing. And though Ive found that killing the power to a machine does this cancel functionality quite nicely, this isnt an option for people at an ATM. So lets implement a giant cancel method which cancels the transaction, no matter what state it is in. How would you do this? Well, using only the knowledge of this article, I would guess that youd put a cancel API method which sends a cancel message to the event. Then in each event you can handle this and go back to the unauthorized state. Witty, but not correct, by no fault of your own! I didnt mention (or I did extremely briefly, you probably missed it) that there is a method gen_fsm:send_all_state_event/2 which sends a message, regardless of what state the server is in. We will use this, to keep our code DRY. Our API: %%-------------------------------------------------------------------- %% Function: cancel/0 %% Description: Cancels the ATM transaction no matter what state. %%-------------------------------------------------------------------- cancel() -> gen_fsm:send_all_state_event(?SERVER, cancel).
This event is sent to the handle_event/3 method, which we extend below: handle_event(cancel, _StateName, _State) -> {next_state, unauthorized, nobody}; handle_event(_Event, StateName, State) -> {next_state, StateName, State}.
If we receive the cancel message, the server resets the state to unauthorized and the state data to nobody: a fresh ATM! As always, the current complete state of eb_atm.erl can be viewed here. Final Notes In this article, I showed you how to create a basic ATM system using gen_fsm. I showed you how to handle messages in a given state, change states, change states with timeouts, and send-to-all messages, among other things. There are some warts in the system though, and I leave it to you to fix them. Here are two exercises for you to do, if you wish. Trust me, you have the ability: 1. Add error checking to deposits. Make it return {error,Reason} and {ok,Balance} instead of just ok all the time. 2. Add a check my balance function to the ATM. This should only be available while authorized, and should not end the transaction. This means that it should not send the user to the thank_you state. This is so that after seeing their balance, the user can withdraw/deposit if necessary. These two features of the exercise wont be used in future articles, and as such I wont post the answers here. You can test them yourself to make sure they work! Part two of this Erlang/OTP series is over. The third article is already in the pipeline and will be published in another few days. It will cover gen_event. For fun, you can think what Ill be adding to ErlyBank using this! I hope you have been enjoying these introductions to Erlang/OTP as much as Ive enjoyed writing them. Thanks for all the support, and good luck! Tags: beginner, erlang, erlybank, gen_fsm, otp introduction Meta: 14 comments, permalink, rss Trackbacks Use this link to trackback from your own site. Comments Leave a response Jay Phillips Sep 08, 2008 10:39 Wow, you keep putting out great blog posts! gen_fsm is definitely something thats not documented well anywhere. Keep up the great work! aurelian Sep 08, 2008 11:30 Mitchell, youre doing an excellent work, really! Halfbaked Ideas Sep 08, 2008 18:23 Intro to gen_fsm and gen_server Emacs njharman Sep 13, 2008 06:33 These are nice intros. A bit of a review for me but your writing style is enjoyable to read and you hit the sweetspot between too simplistic and overwhelming detail. The skeletons are nice too. David Cabana Sep 14, 2008 08:08 Mitchell, My compliments on an outstanding series of posts. Let me ask a question concerning the relationship between this posts authorization mechanism and the previous posts bank account mechanism. Im trying to understand the big picture here. Imagine a potential bad guy is trying to make an unauthorized withdrawal. The security mechanism as illustrated works if the bad guy uses eb_atm:authorize/2 and eb_atm:authorize/1 API. On the other hand, the bad guy could just call eb_server:withdraw/2 and make off with the goods. I understand the system as written is a prototype for didactic purposes. What I am wondering is how one would go about locking it down (I have not read ahead in the series, maybe you address this later). My guess is that one would not register the name of eb_server, and instead allow access to it only by its PID, and then take care that only eb_atm and other authorized processes had access to that PID. Is this the correct approach? David David Cabana Sep 14, 2008 09:04 Mitchell, I hope you wont mind another question. Im trying to understand the concurrency aspects of your example, and also trying to understand the role of the State parameter. As you pointed out, there is an unfortunate overloading of the concept of state in gen_fsm. A finite state automaton has a set of transition states (T-states). In your example these are the states {unauthorized, authorized, thank_you}. In gen_fsm there is another concept of state, a piece of data which Ill call a D-state. The starting state of eb_atm is the pair of {T-state, D-state} == {unauthorized, nobody}. Suppose a user called Joe authenticates. The machine state goes from {unauthorized, nobody} to {authorized, Joe}. Joe is standing at the ATM trying to make up his mind about how much money to withdraw, maybe calling his wife to ask her. What is happening to incoming authentication messages from other users? It looks to me like every clause which handles messages while eb_atm is in the authorized T-state requires the message to provide a D-state parameter matching Joe. Presumably that match does not occur, or authentication is broken. Does this mean the messages are piling up in the process mailbox? If so, isnt the whole authentication mechanism in effect blocking while Joe goes about his business? Thanks, David Mitchell Sep 14, 2008 11:38 Hi David, Great questions! Really great! Ill answer them one at a time So to answer your first question: I dont quite remember (and its far too early for my lazy self to look ) if I mentioned it in this article or the last, but I did mention this security issue. There are a number of ways to solve it. Your idea about not registering the server is one option, but then assuming the malicious user has access to the system, there are always easy ways to look up the PID of a running process. The solution I would use would be to return a session hash after calling eb_server:authorize. The server would maintain a hashmap to map a hash to a given account. In subsequent withdrawal/deposit methods, eb_server would require the hash be sent in instead of the account name. Additionally, hashes should expire after a set amount of time. This is pretty basic security, not at all meant for a true to life bank application. I dont want anyone to think that Ive ever actually meant for this to be a very secure application, as its just meant to show off the features of OTP. To answer your second question: Im only thinking of one ATM terminal in this example, but I do fail to mention that. If there are multiple ATM terminals then the gen_fsm would have to exist in multiple running processes: one for each terminal. I would do this by creating an ATM server for lack of better words. When each ATM terminal boots up, itd register itself with the server, and the server would spawn off a process of eb_atm (not registered), and return the PID to the calling ATM terminal. Then, the terminal can send all requests to this PID. Using this method, it is very easy to add and remove multiple ATM terminals without changes to the code. Hopefully my answers were straightforward enough the two issues you brought up are very insightful but also not truly meant for the beginner-scope of this series. But I enjoy discussing them, so feel free to ask any more Mitchell David Cabana Sep 14, 2008 13:03 Thank, Mitchell, I will keep asking questions! I am not a total beginner at Erlang, but I have never gotten to use it professionally, only some late night dabbling when the kids are asleep. I want to get my head around OTP, and have found there is a severe lack of approachable, simple examples out there, especially of parts other than gen_server. Your gen_fsm example was most appreciated. I look forward to the rest of your series. I had in mind one bank, one ATM server, and many ATM terminals. Your suggestion of one ATM per ATM terminal makes perfect sense. Alain O'Dea Sep 14, 2008 17:22 I have not had a chance to comment up until now, but now that I do This is a brilliant series. It is really helping me dig in on OTP. I think I have had a pretty solid grasp of Erlang the language for, but my grasp of Erlang/OTP as a platform was much less solid. Through articles provide a really good launch point for getting over the hump between knowing Erlang and knowing Erlang/OTP. Thanks David Weldon Sep 29, 2008 11:02 Going back to the multiple FSMs idea from David C: lets say I have a system where I have thousands of live transactions and each one only lives for a few seconds. The nature of each transaction is an FSM, so using gen_fsm makes sense. Like the ATMs, each transaction has its own state so Id need thousands of little servers. Does it make sense that I spawn a new gen_fsm server for each transaction, or should I use some kind of static pool of available servers? An Introduction to gen_server: ErlyBank An introduction to gen_event: Account Notifications spawn_link WordPress | Simplish Mitchell Sep 29, 2008 15:09 David, From what Ive found, when in doubt, just spawn a new process Erlang processes are extremely lightweight and spawn extremely fast. But under extremely heavy traffic, OTP can get a bit heavy. From what Ive experienced, however, its better to just stick to the standards rather than grow-your-own solution. After hitting the OTP limit I would scale horizontally by distributed your Erlang application rather than worry about rewriting the whole thing w/o OTP. Mitchell Denis Laprise Nov 20, 2008 11:05 Hello, Unusual question, but what did you use to draw your sequence diagram? Thanks! John Bender Dec 16, 2008 10:20 Mitchell, What are you using for your syntax highlighting? Matt Mar 05, 2009 08:06 Simple, concise easy to understand article. Thank you