Learn Elixir: OTP, GenServers, Agents, & Tasks

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello and welcome to learn elixir week five module one in this video we're going to be talking about the fundamentals of how we use otp inside of elixir and that's gen servers agents and tasks these three are all process abstractions that allow us to control and manage a process by using an abstraction that makes it easier to actually work with that process each three of these have very defined roles however a few of them do very similar things so it can actually be a bit confusing in which case we use what we'll start out with gen servers which stand for general server these can be used to receive and send messages and have a callback pattern that gets declared in order to handle and receive these messages we can also store state within these gen servers which is how we get state inside of elixir if you've noticed so far we haven't really held any state inside of elixir because we have our database that holds the states for us and each of our calls are simple functions so we don't have a way to actually store data into a specific module we can store state inside of elixir by using a process and so a process can have its own sense of state to do this we normally use gen servers or agents since they have the most well-rounded features in order to manage our state agents are similar to gen servers however they can't receive custom messages you can send messages from any process you want since send is available globally but you can't actually receive a message from within an agent agents are otp's process abstraction around using state simply so if we have some state that doesn't have any behaviors involved in it we would use an agent over a gen server as soon as we start handling some logic with that state itself we probably want to start moving over to a gen server and allow that gen server to code the logic as well as fetch the data from the state agents however are extremely useful because they're a simple abstraction and accessing an agent can actually be quicker than even accessing ets in some cases ets is an erlang term store that's actually created by erlang that allows us to store values and access them concurrently however because agents are so simple it can actually be faster to use the agents instead in some situations our final abstraction is known as a task these can't receive custom messages either similar to an agent so if you're trying to receive messages you're better off with a gen server you also can't give tasks a name so you cannot give a task name because it's not going to be able to receive any messages tasks are often done for a simple one-time use or as a way of setting up polling so that we can use a single process and make our code sequential you can start a task on any node and you can run a task on the current node this gives tasks the power of being able to be started on any single node and you can dynamically scale your tasks across your system it's a very simple process abstraction tasks and it's only responsible for managing one process at a time a single task is akin to a single process with all this in mind let's take a look at some example implementations of using an agent gen server and task i've gone ahead and set up a simple gen server in our gen server we're allowing messages to be added to a queue and then we have another call to be able to fetch all those messages and view them we start a gen server by adding this call to use the gen server this defines the child spec function for us and allows it to set up what a gen server should look like so we can add it to our application tree quite easily we then define a start link function as is standard in elixir with some options how you define the start link function is really up to you but i found this way of defining it to be quite helpful because what you can do is define the options as the defaulted empty argument and then have functions that work based off the assumption that there could be possible values such as put new where we only put the name if a name doesn't already exist and we can then use our options to pass into the gen server start link options this is advantageous because we might want a default name here but when we're creating tests having a default name might actually be a hindrance or perhaps we want to name the server something else or even run multiple copies of this gen server doing so with the default name allows us to actually run this just naturally but also allows us to configure it if we need more depth after start link fires it hits the init function this init function happens directly after start link and is used to set the state into the gen server so our second parameter of this gen server start link was the initial state which is then passed into our init function our init function then returns that in the form of ok and the state after calling start link you'd receive ok and pid and could use that pid to actually call add message or get all messages however for our case we just simply defaulted it to the default name since we're not going to change the name manually you can then use two different functions either cast or call on those gen servers in order to be able to interact with them when you cast to a gen server it does this asynchronously which means the client doesn't wait for a response so if i were to say cast and then io dot inspect hi it would actually run this high before waiting for this cast to complete if we want to wait for something to complete we use call instead so in this case we pass in call with the name or the pid and our message again these messages can be in any format but it's standard and elixir to either use a tuple or to use a singular atom value with our cast we use the tuple value to signify that we wanted to add the message and what that messages value was however in our call we're just trying to get all the messages so we simply used an atom the parameters that go into cast and call are identical so you can pass them in however you want however there is a third parameter on call where you can pass in a timeout so after three seconds if this doesn't complete it times out it gets sent directly to a handle handlecast or handle call callback these callbacks are allowed to access the state and are used by simply specifying the message in the first parameter by using message passing we can define multiple message handlers and simply use the pattern matching to pull out which parameter does what in our message when we cast a message of adding it we simply respond with adding that message and whatever the message is and then prepending that message onto the state we then log out the messages received and reply with absolutely nothing by signifying no reply and returning the new state so in this function we actually took the current state which in our first call would be an empty list like we had in our initial state and then we took the message added that message to the state and then put it back into the process by replying with this tuple we're able to put state back into the process at the end of our function so this is how we can update state as a response to our messages from processes the same thing goes for call however with call we also get a second parameter of a pid this pid is the pid it was called from so if we were to run self out here that pid would be the same pid that we would see from this from pid variable we also have our state which allows us to update it and in our function we simply did a log of handle call checking where it's calling from and then we grabbed the length and returned how many messages were in that message state we then returned the number of messages and the messages itself we then replied with the number of messages as well as all the messages inside of the state by signifying reply that signals that we actually want to reply to the actual function we could also signal no reply here and then the second parameter in the tuple would not have been useful because we're not actually giving a response however with reply we do need three elements inside of the tuple the last element is always the new state of the process our last call is to handle messages that aren't coming in from call or cast if you use send on a gen server it's actually going to get sent to handle info callback so if we were to write send and then our default name with random message this handle info function would get triggered and then it would log out that a random message was hit and not respond but just return the state that it was already passed in we could do some modifications on the statement here however and we could use that in order to modify the state by passing it regular messages instead of using caller cast in a nutshell gen servers are a really solid abstraction for dealing with processes however there are some bottlenecks to be aware of since the messages can only be handled one at a time we'll talk a bit about that in future lessons but for now let's move on to agents i've created a simple agent here where it's also calling the use for agent this similarly to gen server defines a child spec for an agent inside of our agent's options we have the option to give it a name as well and its options look pretty similar to the gen server ones where we use agent.startlink and pass in the agents module that we want to run and then we pass in the initial state and the options this agent actually implements the exact same functionality that we saw inside of the gen server however it's a bit less complex if we go back and open up the agent again we can see that it took us 54 lines of code in order to implement what an agent was 25 so it's about half the lines of code to implement it inside of an agent inside of an agent all we did was define an add message and a get message each of these then used the agent syntax in order to handle that state so what agent syntax is is they have both an update and a get function the update function takes in the name or the pid and then takes in a function for what to do with that state so agents are really responsible for just managing state and only know how to update and get the state of our agent process this agent processes state can then be used to pull out of it or update it so in the case of agent.update we call our update with the function that takes an estate and then append our message onto the state this actually updates the agent state itself and we don't need to do anything further similarly to how we used call to get an asynchronous version where it just returns okay agent also does this when you're calling update to get all the messages we just use agent.get and in this case we take this date and pull the length and then the state out into a tuple this will then just return that tuple back of length and state so agent.get is incredibly quick to use and easy to form state-based processes in cases where you're only using state it's better to use an agent versus a gen server it's kind of a difficult assumption to make but if you find yourself simply pulling or fetching data from a singular gen server process you might want to consider converting it into an agent however if you want to receive messages and start actually modifying that data based off of the messages you're receiving you're going to want to look into a gen server instead our last attraction is tasks in tasks we also have the call for use task similarly to the agent in gen server we also have our start link function but this time instead of defining parameters we only define a function this is because our tasks unlike our agents and gen servers cannot be given a name in this task what we do is we repeatedly log a message out and so this process is kept alive indefinitely what we've done here is simply log out an example polling task with the message that was passed in in the start link function and then we sleep the process for two seconds and call that function again this creates an endless loop of simply calling this example polling task inspection by using process.sleep we're putting the current process to sleep ordinarily this would be an extremely bad thing to do however in our case it's actually completely fine because our task is a singular process and it's isolated itself so if we were to call some code that did this start link and then did some other work that other work could happen in the meantime while this logging was still happening because it's happening in a completely individual and isolated process this is an example of how we can set up a task function as well as how we can set up a gen server and finally an agent as well now that you know a bit more about the basic building blocks of otp and process interaction let's move on to the next video
Info
Channel: Learn Elixir
Views: 969
Rating: 5 out of 5
Keywords:
Id: XrQVmTkVonM
Channel Id: undefined
Length: 13min 5sec (785 seconds)
Published: Tue Feb 23 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.