Next lesson playing in 5 seconds

Cancel
  • Overview
  • Transcript

3.3 Supervisors

Erlang and Elixir embrace a crash and restart model to handle unknown exceptions. The restart part is handled by entities called "supervisors", which you'll learn about in this lesson.

3.3 Supervisors

Hi and welcome back to get started with Elixir. In this lesson, we are going to talk about supervisors and how they manage processes. The reason supervisors exist in Erlang is the way OTP handles errors. Normally, you will try to handle as many situations as you can foresee and prevent the application from crashing. OTP does this a little bit differently. It's philosophy is to let the process crash. Since everything is divided in its own process, this doesn't have much impact on the whole application. And the strategy is actually more reliable than classic error handling. To watch the processes and detect if they crashed, supervisors exist. A supervisor is a process by itself. It needs to be started with the start_link function. You also have to define a strategy how the supervisor should react when a process crashes. The most common one is one_for_one. Which will restart a single process when it crashes. There is also the one_for_all strategy that will restart all child processes if one of them crashes. Then we have rest_for_one, this acts a little bit funny. It will restart all processes that have been created after the process has crashed. We already learned that supervisors are responsible for child process. But this child process could be, again, a supervisor. This hierarchical supervision tree that gets created is what makes Elixir or Erlang very stable. Only if the topmost supervisor dies, the app dies as well. Having a supervision tree also affects how you design apps. You won't think about what classes you need, but what processes. A supervisor is also kept intentionally simple so it won't fail that easily. Of course, when a process crashes its data is lost unless you cache it somewhere. Erlang offers an in-memory and the disk store to do that. Let's focus on an example on how a supervisor works. In the last test we created our Wallet GenServer. It's time we put it under supervision. I'm going to create a new file called wallet_supervisor.ex. Here, I'm defining the WalletSupervisor module, which will use Supervisor. I'm also creating a start_link function that will start the Supervisor as a process. Then, we need an init function. This function receives an empty list as a parameter. Now we have to define our children, I'm going to create a worker that uses the wallet module as a call back with the option wallet 1 in a list. You will see why in a minute. Then we have to call supervise to start the supervision. I'm using the strategy one_for_one, here, which is normally what you want. Now we have to change our wallet module a little bit. Instead of start, we are using start_link, and instead of capturing the wallet, we are going to return everything. Instead of an amount as a parameter, I'm going to use name and add a third parameter to start_link that receives a keyword list for that. The other thing we have to change is the init function. Here, we're going to return ok, 0. So far, so good. But if I would run this now, the wallet wouldn't be persistent, at least for the current supervision tree. Let's create another module called Cache. It's also GenServer. The start_link function will either receive and opts parameter, or initialize it to an empty list. Then we call start_link on the GenServer. I'm going to create two functions here, one to read from the cache, and another to write to it. The first one will call the GenServer with read and a key. And the other will use cost with the write message and the key and value. Now we get to the server API and to caching. In the init function I'm going to call ets.new which is an Erlang module, and name it wallet_store, with the options set and protected. This will create an in-memory store we can use. Then, I need to handle call. Then, I need a handle call function to handle the read. Here, I'm going to look up the key from the table with just the state. I'm wrapping this in a case statement since there might not be a value here yet. If there is one, it will be returned as a two tuple in a list. If we didn't use the set option, we could have start multiple values for the same key. Now or the write, here, we are using ets.insert with a two tuple. This will overwrite the previously start value. We have to change the state to store the name instead of the amount now. Therefore, we'll return name in the init function. And also in the cast and call functions. Instead of storing the amount of the wallet, we can now hand it off to the cache. Therefore, we're going to use Cache.read to get the amount and Cache.write to set it in all our functions. Finally, we have to add the cache to be supervised as well. I'm going to use a keyword list to pass the name as cache. Let's try it out. For reason, I have to manually compile the supervisor in iex, but that's easily done with c wallet_supervisor. After I started the wallet supervisor, I can put money into wallet1 and read the amount. I don't need to start it up anymore. Since I've given my GenServer a name it is identified by. This makes it easier to access a supervised process. We can also kill the wallet by first finding the process with process.whereis and then sending the exit command with a kill option. The supervisor will automatically start a new wallet and we can access it with the same keyboard, the value is still there. To recap, Elixir and Erlang embrace crash and restart instead of excessive error handling. Supervisors watch processes and restart them if they crash. There are multiple strategies for restart but one_for_one is used most of the time. You have to save and restore the state yourself for supervised processes. In the next lesson, you are going to learn more about OTP applications. See you there.

Back to the top